设计目标

资产注册表存在去达成如下的系统设计目标:
1.服务作为一个额外的层在文件路径之上

  • 启用更轻松的资产目录重构
  • 启用分离的Asset和SaveData

2.提供一个OS无关的接口

3.鼓励Packages的使用作为一个方法去存储相关的Assets作为一个集合,它们最常用的地方

4.鼓励Assets的使用作为一个方法去处理文件数据是如何被读取,保存,和管理的

5.通过将数据存储在内存中,防止文件被不必要地重新读取

Abstraction Level1:INI file

努力实现设计目标1,我们可以引用所有的资产通过名字,并将名称和路径之间的字典存储在一个单独的文件中,工作起来像这样。

ini file:

frog_texture = "Assets/Images/Frog/frog_green.png"
bat_texture = "Assets/Images/Bat/bat_texture_new.png"

...etc...

C++ code:

std::unordered_map<std::string, std::string> Assets;

void InitAssetRegistry()
{
    INIFile ini = INILoad("AssetRegistry.ini");
    for(const auto &asset : ini)
    {
        Assets[asset.key] = asset.value;
    }
}

...
void DoFroggyStuff()
{
    FILE* frog_img = fopen(Assets["frog_img"]);
    Render(frog_img);
}

优点:
1.非常简单。
2.当文件被重命名或者移动的时候,你只需要去修改ini文件。

Abstraction Level 2:Assets

理想的Asset应当是一个C++类。通过这种方式,它们将来可以扩展,并且比简单的字符串具有更多的功能。现在我们只需要将其设置为存储操作系统文件路径的std::string。

Abstraction Level 3:Packages

为了解决目标3,让我们创建一个对象,叫它为Package,存储了assets的字典。
这种方式,用户可以收集它们的资产在一个有意义的方式。

为了去实现这个,我们需要去反映packages的概念在ini文件中,对于这个,我们可以使用节头。

[Textures]
frog = "Assets/Images/Frog/frog_green.png"
bat = "Assets/Images/Bat/bat_texture_new.png"

[Levels]
level1 = "Assets/level1.txt"
level2 = "Assets/level2.txt"

Pros:
1.如果实现高效地,packages可以相对地轻松地被修改在未来
2.names可以被重用通过packages
3.package名可以给出线索对于它们内容的类型,那么名字,比如frog_texture可以被减少为
frog在包textures

所以,包是资产的集合,表示一种特定类型的集合。

Abstraction Level 4:Asset Registry System

如果要实现上述所有抽象而不做任何更改,那么最终将得到一个Package类,一个Asset类,并且没有地方去存储它们的示例。抽象Level 1建议使用unordered_map,但是使得这个公有还有全局,将不明智。这个抽象层次实现了一个Asset Registry(资产注册表)命名空间或者单例,提供如下的接口。

void LoadRegistry();//加载资产注册表

void SaveRegistry();//保存资产注册表

std::weak_ptr<Package> GetPackage(const std::string& packageName);//获取包

std::weak_ptr<Package> CreatePackage(const std::string& packageName);//创建包

bool RemovePackage(const std::string& packageName);//移除包

size_t PackageCount();//包数量

Pros:
1.如果实现地高效,后端可以在将来相对容易地进行修改
2.接口给予用户线索,如果使用它来减少对过多文档的需求
3.任何OS确切的操作可以被私有地执行地,并且接口是被全面维护的,这个完整地契合目标2

Additional Features

目前,我们已经实现了一个完整的抽象模型,让我们尝试去解决一些遗留的问题,通过添加一些特性

Additional Feature 1:sub-packages

如果只是简单地使包能够存储对其它包的引用,它们可以工作起来非常类似于符号目录。
下面是INI中可能出现的情况。

[ClassicMode]
SubPackage:Environment = _ClassicEnvironment
SubPackage:Player = _ClassicPlayer

[_ClassicEnvironment]
SubPackage:Textures = _ClassicEnvironmentTextures

[_ClassicEnvironmentTextures]
Asset:Background = Assets/Game/Textures/Background/Background.png
Asset:Boarder = Assets/Game/Textures/Background/Boarder.png
Asset:BrokenCeiling = Assets/Game/Textures/Background/broken_ceiling.png
Asset:DepthMap = Assets/Game/Textures/Background/FlippedDepthMap.png
Asset:Tilemap = Assets/Game/Textures/Tilemap/tile_full_16_metal.png
SubPackage:Particles = _ClassicEnvironmentParticles

[_ClassicEnvironmentParticles]
Asset:RainDrop = Assets/Game/Textures/Particles/rain_drop.png
Asset:RainSplash = Assets/Game/Textures/Particles/rain_splash.png
Asset:Fog = Assets/Game/Textures/Particles/fog.png
Asset:Rocks = Assets/Game/Textures/Particles/rock_bits.png

...etc...

Additional Feature 2:Asset and Package inheritance

我们在提议的系统中仍然有一个很大的局限性,那就是资产仍然只是文件路径,没有办法扩展它们来正确地反序列化数据并提供相关的方法。例如,一个exe资产将理想地有一个"Execute"方法,并且一个sprite asset将理想地存储它的渲染数据在内存中,在它加载之后。感谢,C++提供继承让我么解决这个问题。我们简单地需要去使得我们的getters模板,转换到请求的类型:

template<typename PackageType = Package>
std::weak_ptr<PackageType> GetPackage(const std::string& packageName);

template<typename AssetType = Asset>
std::weak_ptr<AssetType> GetAsset(const std::string& assetName);

对于DeltaBlade引擎,我们觉得实时类型反射是过度杀伤的,那么资产在内部被存储作为std::shared_ptr<Asset>,并且被简单地替换,对于扩展的类型,当GetAsset被调用的时候。
通过正确的运行时类型反射。通过正确的运行类型反射。然而,你可以选择去序列化类型在ini文件,并且然后加载正确的类型在开始阶段。这将允许assets被更加轻松地预加载,而不需要知道外部的类型。

Additional Feature 3:Registry Paths

资产注册表引入了一个概念,称为Registry Paths。类似于文件系统路径,这是一种方式去表示一系列的包,还有打开的子包,以便检索一个asset作为一个冒号描述的字符串。

比如下面:

[UI]
SubPackage:Textures = _UITextures
SubPackage:Audio = _GAME_UI_Audio

[_UITextures]
Asset:MenuButton = Assets/Game/UI/Menu/button.png
Asset:MenuBackground = Assets/Game/UI/Menu/background.png

[_GAME_UI_Audio]
Asset:MenuMusic = Assets/Game/Audio/menu_music.wav
Asset:MenuButtonClick = Assets/Game/Audio/sfs/click.wav

然后取得menu music,可以在任何如下的方式:

// opening packages individually
PackageHandle UI = AssetRegistry::GetPackageChecked("UI").lock();
PackageHandle UIAudio = UI->GetSubPackageChecked("Audio").lock();
std::shared_ptr<AMusic> MenuMusic = UIAudio->GetAssetChecked<AMusic>("MenuMusic").lock();

// using registry paths
std::shared_ptr<AMusic> MenuMusic = AssetRegistry::GetAssetChecked<AMusic>("UI:Audio:MenuMusic").lock();

// using a hybrid
PackageHandle UIAudio = AssetRegistry::GetPackageChecked("UI:Audio").lock();
std::shared_ptr<AMusic> MenuMusic = UIAudio->GetAssetChecked<AMusic>("MenuMusic").lock();

可以看到,注册表路径,UI::Audio::MenuMusic。

Post Mortem

虽然注册表路径在理论上很优秀,它们在实际中鼓励了一种不幸的编码风格。用户将以创建接口,接受一个单一的字符串,表示完整的注册表路径-不同于文件路径。作为结果,注册表不得不去做更多的map查找,并没有按照预定的方式传递包裹。如果我去重新地设计这个,我可能完全地移除这个特性。

Additional Feature 4:Editor

image
开发一个编辑器,使得可以手动进行修改注册表,而不是用文本编辑器。

Additional Feature 5:Error Handling Modals

所有像样的系统有一些形式的错误处理,但是所有伟大的系统可以完整地解决错误,没有crashing。

比如,一个用户重命名一个核心文件,比如默认着色器从"Assets/Game/Shaders/forwardVert.glsl"到"Assets/Game/Shaders/forwardVert_Renamed.glsl",并且忘记修改ini文件。

相反,我让注册表询问用户并等待它们的响应。为了这样做,我不得不去创建一个完整的分离的应用,并且从编辑器登录它,当这里有一个问题的时候。这个应用程序只是简单地引导用户修复它们的错误。

参考资料