我应该如何构建可扩展的资产加载系统?


19

对于Java的业余游戏引擎,我想编写一个简单但灵活的资产/资源管理器。资产是声音,图像,动画,模型,纹理等。经过几个小时的浏览和一些代码实验,我仍然不确定如何设计这个东西。

具体来说,我正在寻找如何设计管理器的方式,以便它抽象出如何加载特定资产类型以及从何处加载资产。我希望能够同时支持文件系统和RDBMS存储,而程序的其余部分不需要了解它。同样,我想添加一个XML动画描述资产(FPS,要渲染的帧,对Sprite图像的引用等)。我应该能够为此编写一个类,该类具有查找和读取XML文件以及创建和返回AnimationAsset带有该信息的类的功能。我正在寻找数据驱动的设计。

我可以找到大量的信息是什么资产管理者应该做的,而不是如何去做。涉及的泛型似乎导致某种形式的类级联,或某种形式的助手类。但是,我还没有看到一个清晰的例子,看起来不像是个人黑客,也不是共识。

Answers:


23

首先,我不考虑资产经理。用松散定义的术语(例如“ manager”)来思考您的体系结构倾向于使您在脑海中扫清许多细节,因此,解决方案变得更加困难。

关注您的特定需求,这似乎与创建一种资源加载机制有关,该机制抽象了基础的原始存储并支持受支持的类型集的可扩展性。您的问题中没有真正涉及的内容,例如,缓存已加载的资源-很好,因为按照单一职责原则,您可能应该将资产缓存构建为单独的实体并在其他位置聚合两个接口, 作为适当的。

为了解决您的特定问题,您应该设计加载器,使其不进行任何资产本身的加载,而是将这种责任委托给为加载特定类型的资产而定制的接口。例如:

interface ITypeLoader {
  object Load (Stream assetStream);
}

您可以创建实现此接口的新类,并定制每个新类以从流中加载特定类型的数据。通过使用流,可以针对与存储无关的公共接口编写类型加载器,而不必进行硬编码就可以从磁盘或数据库中加载。这样甚至可以让您从网络流中加载资产(当您的游戏在控制台上运行,而编辑工具在联网的PC上运行时,这对于实现资产的热重装非常有用)。

您的主要资产加载者需要能够注册和跟踪这些类型特定的加载者:

class AssetLoader {
  public void RegisterType (string key, ITypeLoader loader) {
    loaders[key] = loader;
  }

  Dictionary<string, ITypeLoader> loaders = new Dictionary<string, ITypeLoader>();
}

此处使用的“键”可以是您喜欢的任何东西,它不必是字符串,但是很容易入手。密钥将决定您希望用户如何识别特定资产,并将用于查找适当的加载程序。因为您想隐藏实现可能正在使用文件系统或数据库的事实,所以您不能让用户通过文件系统路径或类似方式引用资产。

用户应仅使用最少的信息来引用资产。在某些情况下,仅一个文件名就足够了,但是我发现使用类型/名称对通常是可取的,因此一切都非常明确。因此,用户可能将您的动画XML文件之一的命名实例称为"AnimationXml","PlayerWalkCycle"

在这里,AnimationXml将是您注册的密钥AnimationXmlLoader,它实现了IAssetLoader。显然,可以PlayerWalkCycle识别特定资产。给定类型名称和资源名称后,资产加载程序可以查询其持久性存储以获取该资产的原始字节。由于我们将在此处获得最大的通用性,因此您可以通过在创建加载器时为加载器传递一种存储访问方式来实现此目的,从而允许您用以后可以提供流的任何内容替换存储介质:

interface IAssetStreamProvider {
  Stream GetStream (string type, string name);
}

class AssetLoader {
  public AssetLoader (IAssetStreamProvider streamProvider) {
    provider = streamProvider;
  }

  object LoadAsset (string type, string name) {
    var loader = loaders[type];
    var stream = provider.GetStream(type, name);

    return loader.Load(stream);
  }

  public void RegisterType (string type, ITypeLoader loader) {
    loaders[type] = loader;
  }

  IAssetStreamProvider provider;
  Dictionary<string, ITypeLoader> loaders = new Dictionary<string, ITypeLoader>();
}

一个非常简单的流提供程序只需在指定的资产根目录中查找名为的子目录,然后将命名type文件的原始字节加载name到流中并返回它。

简而言之,这里的系统是:

  • 有一个类知道如何从某种后端存储(磁盘,数据库,网络流等)读取原始字节。
  • 有些类知道如何将原始字节流转换为特定类型的资源并将其返回。
  • 您实际的“资产加载器”仅包含上述内容,并且知道如何将流提供程序的输出通过管道传输到特定类型的加载器中,从而产生具体的资产。通过公开配置流提供程序和特定于类型的加载程序的方法,您可以拥有一个可由客户端(或您自己)扩展的系统,而无需修改实际的资产加载程序代码。

一些注意事项和最后说明:

  • 上面的代码基本上是C#,但是应该毫不费力地转换为几乎任何语言。为方便起见,我省略了很多东西,例如错误检查或正确使用IDisposable以及其他可能不适用于其他语言的习惯用法。这些留给读者作为家庭作业。

  • 同样,我object如上所述返回具体资产,但是如果愿意,您可以使用泛型或模板或任何其他方式生成更具体的对象类型(应该使用,这很不错)。

  • 如上所述,这里根本不处理缓存。但是,您可以轻松添加缓存,并且具有相同的通用性和可配置性。试试看!

  • 有很多方法可以做到这一点,当然也没有一种方法或共识,这就是为什么您找不到一个的原因。我试图提供足够的代码来传达具体的要点,而不会将此答案变成痛苦的长代码墙。它已经很长了。如果您有任何疑问,请随时发表评论或在聊天中找到我。


1
好的问题和好的答案不仅使解决方案朝着数据驱动的设计方向发展,而且还推动了如何以数据驱动的方式开始思考。
Patrick Hughes

非常好的和深入的答案。我喜欢您如何解释我的问题,并准确地告诉我我在制定如此差的问题时需要知道的内容。谢谢!碰巧,您能指出我一些有关Streams的资源吗?
user8363 '08

“流”只是字节或数据的序列(可能没有确定的结尾)。我特别在考虑C#的Stream,但您可能对Java的类更感兴趣-尽管警告我我对Java的了解不多,所以可能不是理想的类。

流通常是有状态的,因为给定的流对象通常在流中具有当前读取或写入位置,并且您对它执行的任何IO都从该位置发生-这就是为什么我将它们用作上述资产接口的输入的原因,因为他们实际上是在说:“这里有一些原始数据,从哪里开始读取,读取并执行您的操作”。

这种方法遵循SOLIDOOP的一些核心原则。太棒了
亚当·内洛
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.