BlueRoseNote/03-UnrealEngine/Gameplay/Lyra/UE5 Lyra学习笔记(4)—Inventory.md
2023-06-29 11:55:02 +08:00

191 lines
11 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: UE5 Lyra学习笔记(4)—Inventory
date: 2022-08-10 13:55:20
tags: Lyra Gameplay
rating: ⭐️⭐️
---
# 前言
ULyraInventoryManagerComponent通过GameFeature功能在模块激活时挂载Controller上具体可以查看ShooterCore的GameFeature配置文件。
Lyra中的物品系统采用 **Entry - Instance - Definition - Fragment**的结构。但该系统有没有完成的痕迹,所以个人不建议直接拿来用,但里面有考虑到网络同步的设计值得学习。
与ActionRPG项目相比它没有实现SaveGame/LoadGame部分也没有使用FPrimaryAssetId来减少网络同步以及存档大小。所以也挺可惜的。
Inventory的主要的资产为ULyraInventoryItemDefinition前缀名为**ID_**Equipment的主要资产为ULyraEquipmentDefinition前缀名为**WID_** 。这些资产位于:
- `Plugins\GameFeatures\ShooterCore\Content\Items\`
- `Plugins\GameFeatures\ShooterCore\Content\Weapons\`
# Inventory
## Class
- ULyraInventoryManagerComponent整个背包系统的管理组件。使用FLyraInventoryList来管理物品数据。
- FLyraInventoryList类型为`FFastArraySerializer`(为了解决网络传输而实现的类)。内部使用`TArray<FLyraInventoryEntry> Entries`来存储物品数据。
- FLyraInventoryEntry类型为`FFastArraySerializerItem`(为了解决网络传输而实现的类)。内部使用`ULyraInventoryItemInstance`来存储物品数据内部还有StackCount与LastObservedCount变量该变量不同步
- ULyraInventoryItemInstance物品实例。内部变量有`TSubclassOf<ULyraInventoryItemDefinition> ItemDef`以及一个用于存储物品状态的`FGameplayTagStackContainer`(Lyra定义的Tag堆栈)
- ULyraInventoryItemDefinition物品定义`UCLASS(Blueprintable, Const, Abstract)`。内部变量有`TArray<TObjectPtr<ULyraInventoryItemFragment>> Fragments`也就是说一个ItemDefinition会有多个ItemFragment。另外就是用于显示名称的`FText DisplayName`
- ULyraInventoryItemFragment物品片段`UCLASS(DefaultToInstanced, EditInlineNew, Abstract)`。可以理解为物品信息碎片,一个物品定义由多个碎片组成。
相关UClassMeta的官方文档解释
- Blueprintable将此类公开为用于创建蓝图的可接受基类。默认为`NotBlueprintable`。子类会继承该标签。
- Const此类中的所有属性和函数都是`const`并且导出为`const`。子类会继承该标签。
- Abstract`Abstract`说明符会将类声明为"抽象基类"。阻止用户向关卡中添加此类的Actor。
- DefaultToInstanced此类的所有实例都被认为是"实例化的"。实例化的类(组件)将在构造时被复制。子类会继承该标签。
- EditInlineNew指示可以从虚幻编辑器"属性Property"窗口创建此类的对象,而非从现有资源引用。默认行为是仅可通过"属性Property"窗口指定对现有对象的引用。子类会继承该标签;子类可通过 `NotEditInlineNew` 来覆盖它。
### ItemInstance
该类为物品的**实例**类也是物品的操作类。包含了ItemDefinition之外还记录了一些额外的Runtime状态数据FGameplayTagStackContainer也是一个FFastArraySerializer类还定义各种操作用函数。
### ItemDefinition
该类为定义物品各种属性的**资产**类主要通过ItemFragment进行**碎片化描述**。通过蓝图继承的方式来创建一个个的物品定义。
PS.个人很好奇为什么不把这这个类做成一个DataAsset因为这个类也就是个静态数据类。至少ULyraPickupDefinition继承自UDataAsset。
### ItemFragment
该类为物品碎片化的属性描述类同时附带OnInstanceCreated()虚函数,以实现一些属性变化外的特殊效果。
在Lyra中实现了这一些
- UInventoryFragment_EquippableItem用于指定**ULyraEquipmentDefinition**定义于Equipment类。
- UInventoryFragment_SetStats用于给ItemInstance添加状态标签。
- UInventoryFragment_QuickBarIcon用于指定快捷栏SlateBrush资源以及UI显示名称。
- UInventoryFragment_PickupIcon用于指定骨骼物体枪械或者血包、显示名称以及pad颜色。
### Pickup
定义了Pickup相关类
- FInventoryPickup可以理解为Entry。
- FPickupInstanceInstance。
- FPickupTemplate可以理解为Definition。
- UPickupable与IPickupable接口类。
## AddEntry
给FLyraInventoryList添加Entry的逻辑大致为
1. 检查相关变量
2.`TArray<FLyraInventoryEntry> Entries`添加Entry。
1. 使用NewObject()填充Instance指针。
2. 使用Instance->SetItemDef()设置ItemDefinition类。
3. 通过ItemDefinition取得ItemFragment并且调用接口函数OnInstanceCreated()部分ItemDefinition会执行一些特殊逻辑。
3. 设置Entry的StackCount并设置Entry为Dirty触发更新逻辑。
4. 返回Instance指针。
代码如下:
```c++
ULyraInventoryItemInstance* FLyraInventoryList::AddEntry(TSubclassOf<ULyraInventoryItemDefinition> ItemDef, int32 StackCount)
{
ULyraInventoryItemInstance* Result = nullptr;
check(ItemDef != nullptr);
check(OwnerComponent);
AActor* OwningActor = OwnerComponent->GetOwner();
check(OwningActor->HasAuthority());
FLyraInventoryEntry& NewEntry = Entries.AddDefaulted_GetRef();
NewEntry.Instance = NewObject<ULyraInventoryItemInstance>(OwnerComponent->GetOwner()); //@TODO: Using the actor instead of component as the outer due to UE-127172
NewEntry.Instance->SetItemDef(ItemDef);
for (ULyraInventoryItemFragment* Fragment : GetDefault<ULyraInventoryItemDefinition>(ItemDef)->Fragments)
{
if (Fragment != nullptr)
{
Fragment->OnInstanceCreated(NewEntry.Instance);
}
}
NewEntry.StackCount = StackCount;
Result = NewEntry.Instance;
//const ULyraInventoryItemDefinition* ItemCDO = GetDefault<ULyraInventoryItemDefinition>(ItemDef);
MarkItemDirty(NewEntry);
return Result;
}
```
# Equipment
Lyra的装备系统只使用了**Entry - Instance - Definition**结构。
## Class
- ULyraEquipmentManagerComponent类似ULyraInventoryManagerComponent的装备管理类。
- FLyraEquipmentList
- FLyraAppliedEquipmentEntry
- ULyraEquipmentInstance装备实例。
- ULyraEquipmentDefinition装备定义。
- FLyraEquipmentActorToSpawnSpawn用的相关数据。
- ULyraPickupDefinitionUDataAsset。主要是有ULyraInventoryItemDefinition以及其他美术资产指针。
- ULyraWeaponPickupDefinitionULyraPickupDefinition的子类储存Spawn用的LocationOffset与Scale数据。
- ULyraGameplayAbility_FromEquipmentEquipment类型GA的父类实现2个ULyraEquipmentInstance/ULyraInventoryItemInstance Get函数。子类有2个蓝图与1个c++类。
- ULyraQuickBarComponent
### ULyraEquipmentInstance
OnEquipped ()/OnUnequipped()会调用对用的K2函数BlueprintImplementableEvent具体逻辑在蓝图实现。主要的逻辑就是播放对应的Montage动画。
### ULyraEquipmentDefinition
- `TSubclassOf<ULyraEquipmentInstance> InstanceType`装备类型通过Class类型来判断装备类型。
- `TArray<TObjectPtr<const ULyraAbilitySet>> AbilitySetsToGrant`:装备的附带能力集。
- `TArray<FLyraEquipmentActorToSpawn> ActorsToSpawn` 装备Actor。
## FGameplayTagStack
Lyra里定义了FGameplayTagStackContainer 与FGameplayTagStack这2个类来解决GameplayTagStack的同步问题。
其中TagToCountMap只是本地/服务器用于快速查找GameplayTag的Stack数而维护的Map它会在Add/RemoveStack()之外还会在三个网络同步函数进行对应修改。
- AddStack()
- RemoveStack()
- 网络同步函数
- PreReplicatedRemove()
- PostReplicatedAdd()
- PostReplicatedChange()
## ItemDefintion是否可以做成DataAsset
蓝图类以及非蓝图类资产,例如关卡和数据资产(`UDataAsset` 类的资产实例)。
### 非蓝图资产
参考官方文档[AssetManagement](https://docs.unrealengine.com/4.27/zh-CN/ProductionPipelines/AssetManagement/)
**假如主要资产类型不需要存储蓝图数据,你可以使用非蓝图资产。非蓝图资产在代码中的访问更简单,而且更节省内存。** 如需在编辑器中新建一个非蓝图主资产,请在"高级"内容浏览器窗口中新建一个数据资产,或使用自定义用户界面来创建新关卡等。以这种方式创建资产与创建蓝图类不一样;你创建的资产是类的实例,而非类本身。要访问类,请用 `GetPrimaryAssetObject` 这类C++函数加载它们或者用蓝图函数名称中没有Class。一旦加载后你就可以直接访问它们并读取数据。
>因为这些资产是实例而不是类,所以你无法从它们继承类或其他资产。如果你要这样做,例如,如果你想创建一个子资产,继承其父类的值(除了那些显式覆盖的值),你应该使用蓝图类来代替。
## UI
Lyra没有制作背包部分的逻辑但预留了相关的实现。采用在
- `void FLyraInventoryList::PreReplicatedRemove(const TArrayView<int32> RemovedIndices, int32 FinalSize)`
- `void FLyraInventoryList::PostReplicatedAdd(const TArrayView<int32> AddedIndices, int32 FinalSize)`
- `void FLyraInventoryList::PostReplicatedChange(const TArrayView<int32> ChangedIndices, int32 FinalSize)`
调用BroadcastChangeMessage()来实现传入数据到其他组件的目的。
```c++
/** A message when an item is added to the inventory */
USTRUCT(BlueprintType)
struct FLyraInventoryChangeMessage
{
GENERATED_BODY()
//@TODO: Tag based names+owning actors for inventories instead of directly exposing the component?
UPROPERTY(BlueprintReadOnly, Category=Inventory)
UActorComponent* InventoryOwner = nullptr;
UPROPERTY(BlueprintReadOnly, Category = Inventory)
ULyraInventoryItemInstance* Instance = nullptr;
UPROPERTY(BlueprintReadOnly, Category=Inventory)
int32 NewCount = 0;
UPROPERTY(BlueprintReadOnly, Category=Inventory)
int32 Delta = 0;
};
void FLyraInventoryList::BroadcastChangeMessage(FLyraInventoryEntry& Entry, int32 OldCount, int32 NewCount)
{
FLyraInventoryChangeMessage Message;
Message.InventoryOwner = OwnerComponent;
Message.Instance = Entry.Instance;
Message.NewCount = NewCount;
Message.Delta = NewCount - OldCount;
UGameplayMessageSubsystem& MessageSystem = UGameplayMessageSubsystem::Get(OwnerComponent->GetWorld());
MessageSystem.BroadcastMessage(TAG_Lyra_Inventory_Message_StackChanged, Message);
}
```
### StatTags的作用
物品系统的ULyraInventoryItemInstance中的来记录某个Tag对应的Int32数量在蓝图中调用GetStatTagCount来获取对应的数量。
```c++
UPROPERTY(Replicated)
FGameplayTagStackContainer StatTags;
```
AddStatTagCount在Fragmentstat里被调用用于设置初始的话数量RemoveStatTagCount则在 GameplayEffect的cost中被调用。
PS.可以用来记录 血药的使用量重量32000 用一次=》30000