第十五章:AssetManager工作流程
基本过程
jME3 使用Java I/O来读取资产数据,把整个过程分为三个步骤:
一、打开输入流
这一步通常会根据资源路径(URL)获得一个 InputStream 对象,程序可以通过这个对象来读取数据。输入流的来源可能多种多样,例如:
- 文件路径 new FileInputStream("D:\MyGame\Save\savedata.dat")
- 网络地址 socket.getInputStream();
- 内存地址 new ByteArrayInputStream(data);
- 等等..
不同来源的输入流,需要有不同的处理方法。在 jME3 中,这一步由不同类型的 AssetLocator
负责完成。例如,通过下面的代码可以让 AssetManager 从磁盘文件夹中加载资产。
assetManager.registerLocator("F:\Models", FileLocator.class);
当使用 assetManager 来加载路径为 Models/Monkey/monkey.j3o
的资产时,FileLocator
就会试图通过下面这种方式来打开输入流:
InputStream is = new FileInputStream("F:\Models\Models\Monkey\monkey.j3o");
等 AssetLocator
把输入流搞定后,具体如何读取数据,就会交给第二步来处理。
二、解析数据
游戏中会用到很多不同类型的多媒体资产,需要根据文件格式来解析。
例如jpeg、png、gif等图片文件,先要根据压缩算法对图像数据进行解压,然后恢复成实际的图像数据才能在游戏中使用。
例如fbx、blend、3ds等模型文件,其中的内容可能由上千个不同的数据结构组成,需要针对每一种结构来分别处理,最终得到正确的顶点、法线、纹理坐标等数据来使用。
这些资产数据解析后,一般要生成 jME3 能够识别的专有数据结构,然后才能使用。在 jME3 中,这一步由不同类型的 AssetLoader
负责完成。例如 TGALoader 负责解析 tga 图片,生成 Texture
对象;OBJLoader 负责解析 obj 模型,生成 Spatial
对象。
jME3实现了多种不同的 AssetLoader
,通过下面的代码,可以让 AssetManager 记住文件格式与 AssetLoader
之间的对应关系。
assetManager.registerLoader(TGALoader.class, ".tga");
assetManager.registerLoader(DDSLoader.class, ".dds");
assetManager.registerLoader(OBJLoader.class, ".obj");
assetManager.registerLoader(MTLLoader.class, ".mtl");
AssetManager 会自动根据 文件后缀名 来判断数据格式,找到对应的 AssetLoader 来解析数据。
jME3 专门定义了 AssetKey
类,它的主要作用是保存资产的路径(URL),并分析其后缀名。例如:
AssetKey key = new AssetKey("Models/Monkey/monkey.j3o");
assert key.getName() == "Models/Monkey/monkey.j3o";
assert key.getFolder() == "Models/Monkey/";
assert key.getExtension() == "j3o";
等找到了正确的 AssetLoader
,解析完毕后,输入流就可以关掉了。
三、关闭输入流
这是最简单的,通常只需要调用 InputStream 的 close() 方法即可。
缓存
在进行游戏开发时,很多资产文件都会被重复使用。比如路人NPC的模型,在一个场景中可能会出现很多次。又比如噪声纹理,可能被当做“云”材质使用,也可能被当做“水”材质使用。如果每次使用某个资产时都要从I/O加载,那引擎的性能就太低了。一般的游戏引擎都会缓存游戏资产。
缓存通常是内存中的Hash表,以 <Key,Value> 的形式保存资产。 AssetManager 使用 HashMap
来缓存资产,并定义了 AssetKey
用来查找和缓存资产。
AssetManager 在打开输入流之前,先要创建一个 AssetKey
,并使用它来查找缓存。由于资产的路径(URL)通常是唯一的,jME3默认使用资产的URL来创建 AssetKey。
AssetManager 以 AssetKey 对象作为参数,查找 HashMap 中的资产。结果分为两种:
- 命中缓存:如果发现需要使用的是内存中已经存在的资产,那就直接在内存中 复制(克隆) 一份。这样就不需要走I/O流了,速度会快很多。
- 未命中缓存:如果在HashMap中没有找到资产,那么就走I/O流程。数据解析完毕后,把AssetKey和资产都 存入HashMap中,等下次使用就可以直接加载了。
这个缓存机制可以极大地减少 I/O 操作,但也带来了一些负面影响:
- 热更新失效:如果在程序外部修改了资源内容,希望游戏内能看到改变,但由于资产的URL没变,导致 AssetManager 使用了缓存而不是更新后的文件。
- 垃圾回收失效:Java的内存回收是依赖于底层 GC 的。当 HashMap 中一直保存这对象的引用时,GC 永远不会回收此对象的内存。
这两个问都好解决, AssetManager 中提供了 deleteFromCache(AssetKey key)
方法,可以手动清除缓存;也可以通过 AssetKey 设置不同的缓存机制,这个后续的文章再详细介绍。
文件之间的关联
使用前面介绍的模式来加载资产,流程固然清晰,但也带来了其它问题。
假设 AssetLocator 接口和 AssetLoader 接口是这样定义的:
public interface AssetLocator {
public void setRootPath(String rootPath);
public InputStream locate(AssetKey key);
}
public interface AssetLocator {
public Object load(InputStream in) throws IOException;
}
若 OBJLoader 正在加载一个 obj 文件,其中记录了材质文件(.mtl)的相对路径。必须要需要根据 .obj 文件的 URL 来计算 .mtl 文件的路径,否则就无法加载模型的材质。可是在上面设计的接口中,AssetLoader 只拿到了 InputStream ,而 InputStream 是不保存资源路径的。 这就导致无法加载 .mtl 文件。
这就像一个老板,分别交代甲和乙合作完成一件事,但是禁止甲和乙私下沟通。现在乙要做一件事,可这件事只有甲才知道怎么办,乙就懵逼了。
jME3的接口当然没有这种低级错误,它使用 AssetInfo
来解决了这个问题。解决方法很简单,就是把 AssetManager
、AssetKey
、InputStream
等东西都保存在 AssetInfo 对象中,然后一起传给 AssetLoader。
AssetInfo
的定义如下:
import java.io.InputStream;
public abstract class AssetInfo {
protected AssetManager manager;
protected AssetKey key;
public AssetInfo(AssetManager manager, AssetKey key) {
this.manager = manager;
this.key = key;
}
public AssetKey getKey() { return key; }
public AssetManager getManager() { return manager; }
@Override
public String toString(){
return getClass().getName() + "[" + "key=" + key + "]";
}
public abstract InputStream openStream();
}
其中的主要方法如下:
openStream()
方法,返回InputStream
对象,用于解析数据;getKey()
方法,返回AssetKey
对象,通过它就能够得到资产路径;getManager()
方法,返回AssetManager
对象,可以用它来加载相关的纹理、材质等资产。
jME3 中实际的 AssetLocator 和 AssetLoader 接口设计是这样的:
public interface AssetLocator {
public void setRootPath(String rootPath);
public AssetInfo locate(AssetManager manager, AssetKey key);
}
public interface AssetLocator {
public Object load(AssetInfo info) throws IOException;
}
小结
经过前面的介绍,下面重新梳理一下AssetManager的工作流程。
具体的资源加载步骤如下:
- 根据资产的URL,创建AssetKey。
- 根据AssetKey去缓存中查找资源。
- 若AssetKey命中缓存,则复制资源内容,直接返回。
- 若AssetKey未命中,各种AssetLocator将去根目录下搜索对应的URL。若找不到资产,则抛出 AssetNotFoundException。
- 若AssetLocator找到资源,则打开输入流,输出AssetInfo对象。
- AssetLoader根据输入的AssetInfo,解析资源数据。
- 将解析结果保存到缓存。
- 关闭输入流。
需要注意的是,上述流程并不完整。在第6步和第7步之间,实际上还有一个后期处理过程。因为AssetLoader返回的对象有时并不能直接被游戏引擎使用。
比如各种图片文件,解析的结果是一个Image
对象,但游戏需要使用的是Texture
对象。jME3 设计了 AssetProcessor 接口,用于对 AssetLoader 生成的对象进行后期处理,转换成实际的对象类型。
但是AssetProcessor在整个流程中的存在感很弱,一般情况下可以当它不存在,所以我就不仔细介绍了。在以后的文章中,如果需要用到 AssetProcessor,我再详细分析。