BlueRose
文章97
标签28
分类7
AssetManager系列之actionRPG项目中的RPGItem类

AssetManager系列之actionRPG项目中的RPGItem类

前言

AssetManager是一个全局单例类,用于管理各种primary assets和asset bundles。配合配套工具 “(Reference Viewer)、“资源审计(Asset Audit)”可以理清Pak文件中Asset的依赖关系以及所占用的空间。

自定义AssetManager可以实现自定义Asset、自定义Asset载入逻辑与控制(异步与同步载入、获取载入进度、运行时修改载入优先级、运行时异步加载强制等待等)、通过网络下载Asset并载入、烘焙数据分包(打包成多个pak)。

在actionRPG项目中通过继承UPrimaryDataAsset来实现自定义DataAsset文件(content browser中能创建的Asset),具体请参照RPGItem,其中包含了包含了文字说明(FTEXT)、物品图标(FSlateBrush)、价格(int32)、相关能力(UGameplayAbility),具体的之后会介绍。

在ShooterGame中实现了如何将资源分包(打包多个pak)。

本文将解析actionRPG中所用相关功能,另外功能会在之后的文章中解析。本人参考的资料如下:

文档地址:
https://docs.unrealengine.com/zh-CN/Engine/Basics/AssetsAndPackages/AssetManagement/index.html
https://docs.unrealengine.com/en-US/Engine/Basics/AssetsAndPackages/AssetManagement/CookingAndChunking/index.html

在这一篇文档中,介绍了使用Primary Asset Labels与Rules Overrides对所有需要烘焙的数据进行分包处理(打包成多个Pak文件),这个操作也需要实现自定义的AssetManager类,具体的可以参考ShooterGame案例。

AnswerHUB、论坛帖子与wiki:

从网上下载Asset并使用StreamableManagers加载:(文章较旧仅供参考)
https://answers.unrealengine.com/questions/109485/stream-an-asset-from-the-internet.html

TAssetPtr与Asset异步加载:
https://wiki.unrealengine.com/index.php?title=TAssetPtr_and_Asynchronous_Asset_Loading

(代码较旧仅供参考)
https://github.com/moritz-wundke/AsyncPackageStreamer

在移动端版本更新的工具蓝图函数(和本文内容关系不大)
https://docs.unrealengine.com/en-US/Engine/Blueprints/UserGuide/PatchingNodes/index.html

几种Asset加载方法(文章较旧仅供参考)
https://www.sohu.com/a/203578475_667928

谁允许你直视本大叔的 的Blog

Saeru_Hikari 的Blog

动作游戏框架子模块剖析(其一)———DataAsset:https://www.bilibili.com/read/cv2855601/

AssetManager及相关名词简述

AssetManager可以使得开发者更加精确地控制资源发现与加载时机。AssetManager是存在于编辑器游戏中的单例全局对象,用于管理primary assets和asset bundles,我们可以根据自己的需求去重写它。

Assest

Asset指的是在Content Browser中看到的那些物件。贴图,BP,音频和地图等都属于Asset文件。

Asset Registry

Asset Registry是Asset注册表,位于Project Settings——AssetManager中(Primany Asset Types To Scan),其中存储了每个的asset的有用信息。这些信息会在asset被储存的时候进行更新。

Streamable Managers

Streamable Managers负责进行读取物件并将其放在内存中.

Asset Bundle

Asset Bundle是一个Asset的列表,用于将一堆Asset在runtime的时候载入。

Primary Assets、Secondary Assets与Primary Asset Labels

AssetManagementSystem将资源分为两类:PrimaryAssetsSecondaryAssets

PrimaryAssets

PrimaryAssets可以通过,调用GetPrimaryAssetId()获取的PrimaryAssetID对其直接操作。

将特定UObject类构成的资源指定PrimaryAssets,需要重写GetPrimaryAssetId函数,使其返回有效的一个有效的FPrimaryAssetId结构。

SecondaryAssets

SecondaryAssets不由AssetManagementSystem直接处理,但其被PrimaryAssets引用或使用后引擎便会自动进行加载。默认情况下只有UWorld(关卡Asset )为主资源;所有其他资源均为次资源。

SecondaryAssets设为PrimaryAssets,必须重写GetPrimaryAssetId函数,返回一个有效的 FPrimaryAssetId结构。

自定义DataAsset

这里我将通过解读actionRPG中的做法来进行介绍:

编写自定义AssetManager

  • 继承UAssetManager创建URPGAssetManager类。
  • 实现单例类所需的static函数Get()。
static URPGAssetManager& Get();
URPGAssetManager& URPGAssetManager::Get()
{
    //直接从引擎获取指定的AssetManager
    URPGAssetManager* This = Cast<URPGAssetManager>(GEngine->AssetManager);
    if (This)
    {
        return *This;
    }
    else
    {
        UE_LOG(LogActionRPG, Fatal, TEXT("Invalid AssetManager in DefaultEngine.ini, must be RPGAssetManager!"));
        return *NewObject<URPGAssetManager>(); // never calls this
    }
}
  • 除此之外还重写了StartInitialLoading函数,用于在AssetManager初始化扫描PrimaryAsset后初始化GameplayAbility的数据;以及实现了用于强制加载RPGItem类Asset的ForceLoadItem函数(RPGItem为之后创建的自定义DataAsset类)。
void URPGAssetManager::StartInitialLoading()
{
    Super::StartInitialLoading();
    UAbilitySystemGlobals::Get().InitGlobalData();
}
URPGItem* URPGAssetManager::ForceLoadItem(const FPrimaryAssetId& PrimaryAssetId, bool bLogWarning)
{
    FSoftObjectPath ItemPath = GetPrimaryAssetPath(PrimaryAssetId);

    //使用同步方法来载入Asset,TryLoad内部使用了StaticLoadObject与LoadObject
    URPGItem* LoadedItem = Cast<URPGItem>(ItemPath.TryLoad());

    if (bLogWarning && LoadedItem == nullptr)
    {
        UE_LOG(LogActionRPG, Warning, TEXT("Failed to load item for identifier %s!"), *PrimaryAssetId.ToString());
    }

    return LoadedItem;
}
  • 在Project Settings-Engine-General Settings-Default Classes-Asset Manager Class中指定你创建的AssetManager。

编写自定义DataAsset

  • 继承UPrimaryDataAsset创建URPGItem类。
  • 重写GetPrimaryAssetId(),以此让AssetManager“认识”我们写的DataAsset
FPrimaryAssetId URPGItem::GetPrimaryAssetId() const
{
    //因为这里URPGItem会被作为一个DataAsset使用而不是一个blueprint,所以我们可以使用他的FName。
    //如果作为blueprint,就需要手动去掉名字中的“_C”
    return FPrimaryAssetId(ItemType, GetFName());
}
  • 声明所需的变量,实现所需的函数
class URPGGameplayAbility;

/** Base class for all items, do not blueprint directly */
UCLASS(Abstract, BlueprintType)
class ACTIONRPG_API URPGItem : public UPrimaryDataAsset
{
    GENERATED_BODY()

public:
    /** Constructor */
    URPGItem()
        : Price(0)
        , MaxCount(1)
        , MaxLevel(1)
        , AbilityLevel(1)
    {}

    /** Type of this item, set in native parent class */
    UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = Item)
    FPrimaryAssetType ItemType;

    /** User-visible short name */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Item)
    FText ItemName;

    /** User-visible long description */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Item)
    FText ItemDescription;

    /** Icon to display */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Item)
    FSlateBrush ItemIcon;

    /** Price in game */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Item)
    int32 Price;

    /** Maximum number of instances that can be in inventory at once, <= 0 means infinite */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Max)
    int32 MaxCount;

    /** Returns if the item is consumable (MaxCount <= 0)*/
    UFUNCTION(BlueprintCallable, BlueprintPure, Category = Max)
    bool IsConsumable() const;

    /** Maximum level this item can be, <= 0 means infinite */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Max)
    int32 MaxLevel;

    /** Ability to grant if this item is slotted */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Abilities)
    TSubclassOf<URPGGameplayAbility> GrantedAbility;

    /** Ability level this item grants. <= 0 means the character level */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Abilities)
    int32 AbilityLevel;

    /** Returns the logical name, equivalent to the primary asset id */
    UFUNCTION(BlueprintCallable, Category = Item)
    FString GetIdentifierString() const;

    /** Overridden to use saved type */
    virtual FPrimaryAssetId GetPrimaryAssetId() const override;
};
bool URPGItem::IsConsumable() const
{
    if (MaxCount <= 0)
    {
        return true;
    }
    return false;
}

FString URPGItem::GetIdentifierString() const
{
    return GetPrimaryAssetId().ToString();
}

FPrimaryAssetId URPGItem::GetPrimaryAssetId() const
{
    return FPrimaryAssetId(ItemType, GetFName());
}

-之后为了保证FPrimaryAssetType统一,我们可以到URPGAssetManager中添加几个FPrimaryAssetType类型全局静态变量,并在cpp文件中进行赋值。

class ACTIONRPG_API URPGAssetManager : public UAssetManager
{
    //上略

    static const FPrimaryAssetType    PotionItemType;
    static const FPrimaryAssetType    SkillItemType;
    static const FPrimaryAssetType    TokenItemType;
    static const FPrimaryAssetType    WeaponItemType;

    //下略
}
//在cpp文件中进行赋值
const FPrimaryAssetType    URPGAssetManager::PotionItemType = TEXT("Potion");
const FPrimaryAssetType    URPGAssetManager::SkillItemType = TEXT("Skill");
const FPrimaryAssetType    URPGAssetManager::TokenItemType = TEXT("Token");
const FPrimaryAssetType    URPGAssetManager::WeaponItemType = TEXT("Weapon");

之后就可以在URPGItem的派生类中通过这些全局变量给FPrimaryAssetType赋值了,例如:

URPGPotionItem()
{
    ItemType = URPGAssetManager::PotionItemType;
}
  • 最后一步就是编写相应的URPGItem派生类,并在在Project Settings——Game——Asset Manager——Primany Asset Types To Scan中添加。注意PrimanyAssetType必须填写正确,不然引擎是搜索不到的。

数据加载

项目中会通过ARPGPlayerControllerBase中LoadInventory函数加载DataAsset数据。最终会使用AssetManager.ForceLoadItem来加载Asset(PS.在构造函数中会调用LoadInventory)

bool ARPGPlayerControllerBase::LoadInventory()
{
    InventoryData.Reset();
    SlottedItems.Reset();

    // Fill in slots from game instance
    UWorld* World = GetWorld();
    URPGGameInstanceBase* GameInstance = World ? World->GetGameInstance<URPGGameInstanceBase>() : nullptr;

    if (!GameInstance)
    {
        return false;
    }

    for (const TPair<FPrimaryAssetType, int32>& Pair : GameInstance->ItemSlotsPerType)
    {
        for (int32 SlotNumber = 0; SlotNumber < Pair.Value; SlotNumber++)
        {
            SlottedItems.Add(FRPGItemSlot(Pair.Key, SlotNumber), nullptr);
        }
    }

    URPGSaveGame* CurrentSaveGame = GameInstance->GetCurrentSaveGame();
    URPGAssetManager& AssetManager = URPGAssetManager::Get();
    if (CurrentSaveGame)
    {
        // Copy from save game into controller data
        bool bFoundAnySlots = false;
        for (const TPair<FPrimaryAssetId, FRPGItemData>& ItemPair : CurrentSaveGame->InventoryData)
        {
            URPGItem* LoadedItem = AssetManager.ForceLoadItem(ItemPair.Key);

            if (LoadedItem != nullptr)
            {
                InventoryData.Add(LoadedItem, ItemPair.Value);
            }
        }

        for (const TPair<FRPGItemSlot, FPrimaryAssetId>& SlotPair : CurrentSaveGame->SlottedItems)
        {
            if (SlotPair.Value.IsValid())
            {
                URPGItem* LoadedItem = AssetManager.ForceLoadItem(SlotPair.Value);
                if (GameInstance->IsValidItemSlot(SlotPair.Key) && LoadedItem)
                {
                    SlottedItems.Add(SlotPair.Key, LoadedItem);
                    bFoundAnySlots = true;
                }
            }
        }

        if (!bFoundAnySlots)
        {
            // Auto slot items as no slots were saved
            FillEmptySlots();
        }

        NotifyInventoryLoaded();

        return true;
    }

    // Load failed but we reset inventory, so need to notify UI
    NotifyInventoryLoaded();

    return false;
}