前言
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:
- Unreal Engine 4 —— Asset Manager介绍:https://blog.csdn.net/noahzuo/article/details/78815596
- Unreal Engine 4 —— Fortnite中的Asset Manager与资源控制:https://blog.csdn.net/noahzuo/article/details/78892664
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将资源分为两类:PrimaryAssets与SecondaryAssets。
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;
}