第十九章:自定义AssetLoader
用Java I/O读取文件,分为三步:
- 打开输入流
- 读取数据
- 关闭输入流
在jME3中,“读取数据”这一步是由资产加载器(AssetLoader)来实现的。AssetLoader是一个接口,只定义了一个方法:
public interface AssetLoader {
public Object load(AssetInfo info);
}
当 AssetManager 在加载游戏资产时,先会调用各种 AssetLocator 的 locate(AssetKey key)
方法去搜索资源,并返回一个 AssetInfo
对象。然后根据文件的后缀名来选择一个 AssetLoader
类,调用它的 load(AssetInfo info)
方法来读取和解析数据,最后返回一个具体的对象。
实现TextLoader
为了演示 AssetLoader 的具体工作方式,下面实现一个自定义的 TextLoader 类。
package net.jmecn.assets;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader;
/**
* 文本文件解析器
* @author yanmaoyuan
*
*/
public class TextLoader implements AssetLoader {
@Override
public Object load(AssetInfo assetInfo) throws IOException {
// 打开输入流
InputStream in = assetInfo.openStream();
Scanner scanner = new Scanner(in);
// 读取文件
StringBuffer sb = new StringBuffer();
String line;
while(scanner.hasNextLine()) {
line = scanner.nextLine();
sb.append(line);
sb.append("\n");
}
// 关闭输入流
scanner.close();
in.close();
return sb.toString();
}
}
这个类的作用一目了然:逐行读取输入流中的文本数据,最后返回了整个字符串。
创建测试文件
为了测试这个TextLoader的作用,在工程目录下创建 assets
文件夹,并在 assets
目录中创建 A.txt
、B.xml
、C.md
三个文件,它们的内容如下。
assets/A.txt
Hello TextLoader!
This is A.txt
assets/B.xml:
<root>
<title>Hello TextLoader</title>
<context>This is B.xml</context>
</root>
assets/C.md
Hello `TextLoader`!
This is **C.md**.
编写测试类
只需要有一个AssetInfo对象作为参数,就可以调用 AssetLoader 接口,从而绕过 AssetManager。
事实上,我们连测试文件都不需要,可以直接在内存中定义一个字符串,然后利用ByteArrayInputStream来把字节数据包装成InputStream。
package net.jmecn.assets;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader;
/**
* 测试 TextLoader
* @author yanmaoyuan
*
*/
public class TestTextLoader {
public static void main(String[] args) {
// 打开输入流
AssetInfo info = new AssetInfo(null, null) {
@Override
public InputStream openStream() {
String text = "Hello TextLoader!\nThis is a string in memoery";
byte[] data = text.getBytes();
return new ByteArrayInputStream(data);
}
};
// 解析文件
AssetLoader loader = new TextLoader();
try {
String result = (String)loader.load(info);
System.out.println(result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
Hello TextLoader!
This is a string in memoery
不过正常来说,从文件中读取数据是更常用。
package net.jmecn.assets;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetLoader;
/**
* 测试 TextLoader
* @author yanmaoyuan
*
*/
public class TestTextLoader {
public static void main(String[] args) {
// 打开文件流
AssetInfo info = new AssetInfo(null, null) {
@Override
public InputStream openStream() {
InputStream in = null;
try {
in = new FileInputStream("assets/A.txt");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return in;
}
};
// 解析文件
AssetLoader loader = new TextLoader();
try {
String result = (String)loader.load(info);
System.out.println(result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
Hello TextLoader!
This is A.txt
前文已经介绍过AssetManager、AssetLocator的作用,使用它们来测试AssetLoader,做法如下。
package net.jmecn.assets;
import java.io.IOException;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLoader;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.system.JmeSystem;
/**
* 测试 TextLoader
* @author yanmaoyuan
*
*/
public class TestTextLoader {
public static void main(String[] args) {
AssetManager assetManager = JmeSystem.newAssetManager();
assetManager.registerLocator("assets", FileLocator.class);
// 打开输入流
AssetInfo info = assetManager.locateAsset(new AssetKey("B.xml"));
// 解析文件
AssetLoader loader = new TextLoader();
try {
String result = (String)loader.load(info);
System.out.println(result);
} catch (IOException e) {
e.printStackTrace();
}
}
}
运行结果:
<root>
<title>Hello TextLoader</title>
<context>This is B.xml</context>
</root>
关联后缀名
调用 AssetManager
中的 registerLoader
方法,可以把某种 AssetLoader 实现类与特定的后缀名关联在一起。在调用 loadAsset
方法时,AssetManager 会根据后缀名去匹配 AssetLoader
。
package net.jmecn.assets;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.system.JmeSystem;
/**
* 测试 TextLoader
*
* @author yanmaoyuan
*
*/
public class TestTextLoader {
public static void main(String[] args) {
AssetManager assetManager = JmeSystem.newAssetManager();
assetManager.registerLocator("assets", FileLocator.class);
assetManager.registerLoader(TextLoader.class, "md");
// 加载资产
String result = (String) assetManager.loadAsset("C.md");
System.out.println(result);
}
}
运行结果:
Hello `TextLoader`!
This is **C.md**.
如果希望让一个 AssetLoader 关联多种文件类型,可以这样做:
package net.jmecn.assets;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.system.JmeSystem;
/**
* 测试 TextLoader
*
* @author yanmaoyuan
*
*/
public class TestTextLoader {
public static void main(String[] args) {
AssetManager assetManager = JmeSystem.newAssetManager();
assetManager.registerLocator("assets", FileLocator.class);
// 关联文件后缀名
assetManager.registerLoader(TextLoader.class, "txt");
assetManager.registerLoader(TextLoader.class, "xml");
assetManager.registerLoader(TextLoader.class, "md");
// 加载资产
String result = (String) assetManager.loadAsset("C.md");
System.out.println(result);
}
}
还有更简单的做法:
package net.jmecn.assets;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.system.JmeSystem;
/**
* 测试 TextLoader
*
* @author yanmaoyuan
*
*/
public class TestTextLoader {
public static void main(String[] args) {
AssetManager assetManager = JmeSystem.newAssetManager();
assetManager.registerLocator("assets", FileLocator.class);
// 关联文件后缀名
assetManager.registerLoader(TextLoader.class, "txt", "xml", "md");
// 加载资产
String result = (String) assetManager.loadAsset("C.md");
System.out.println(result);
}
}
这两种做法的结果都是一样的。
类型转换
注意,load(AssetInfo info)
方法的返回类型是 Object
。由于 Java 中的所有类都是 java.lang.Object
的子类,因此 AssetLoader 才能支持扩展解析任意类型的对象。
问题是 AssetLoader 在返回解析后的对象时,原始的对象类型丢失了,需要进行强制转换才能获得原来的类型。
// 加载资产
String result = (String) assetManager.loadAsset("C.md");
System.out.println(result);
这样进行对象的强制转换,在Java中称为向上转型,是一种不安全的操作。如果类型比匹配,就会产生 ClassCastException
。jME3因此设计了“泛型”机制,可以通过AssetKey来指定加载类型。
// 加载资产
AssetKey<String> key = new AssetKey<String>("C.md");
String result = assetManager.loadAsset(key);
System.out.println(result);
使用泛型的前提,是开发者能够确定某种资产返回的数据类型。在JME3中,有几类游戏专用数据类型,已经定义好了对应的泛型AssetKey。
- 3D模型,返回Spatial类型。定义为
public class ModelKey extends AssetKey<Spatial>
- 纹理,返回Texture类型。定义为
public class TextureKey extends AssetKey<Texture>
- 材质,返回Material类型。定义为
public class MaterialKey extends AssetKey<Material>
- 音频文件,返回AudioData。定义为
public class AudioKey extends AssetKey<AudioData>
如果你需要加载某种特定类型的资产,最好使用泛型AssetKey来限定。
解析自定义3D模型
为了进一步演示如何使用AssetLoader,下面模仿OBJ格式,定义一种简单的3D模型格式。
.yan
格式是纯文本文件,每一行表示一种数据结构。#
开头表示是注释,可忽略。v
开头表示三维顶点,后面连续3个浮点数,定义了顶点坐标(x, y, z)。t
开头表示纹理坐标,后面连续2个浮点数,定义了纹理坐标(u, v)。f
开头表示是面,后面连续3个整数,定义了三角形的顶点索引。
样例文件如下:model.yan
# Vertex
v 0.0, 0.0, 0.0
v 1.0, 0.0, 0.0
v 1.0, 1.0, 0.0
v 0.0, 1.0, 0.0
# TexCoord
t 0.0, 0.0
t 1.0, 0.0
t 1.0, 1.0
t 0.0, 1.0
# Face
f 0, 1, 2
f 0, 2, 3
在assets目录下创建文本文件 model.yan
,并把上述内容写到该文件中。
实现YanLoader
定义 YanLoader 类,用于解析 .yan
格式的文件。
package net.jmecn.assets;
import java.io.IOException;
import java.io.InputStream;
import java.nio.FloatBuffer;
import java.nio.ShortBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLoader;
import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.util.BufferUtils;
/**
* 解析.yan格式的模型文件
* @author yanmaoyuan
*
*/
public class YanLoader implements AssetLoader {
private List<Float> vertex = new ArrayList<Float>();
private List<Float> texCoord = new ArrayList<Float>();
private List<Integer> face = new ArrayList<Integer>();
private AssetManager assetManager;
private AssetKey key;
@Override
public Object load(AssetInfo assetInfo) throws IOException {
assetManager = assetInfo.getManager();
key = assetInfo.getKey();
// 解析文件数据
parse(assetInfo);
// 生成3D网格
Mesh mesh = buildMesh();
// 创建几何体
Geometry geom = new Geometry(key.getName());
geom.setMesh(mesh);
// 加载材质
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.White);
geom.setMaterial(mat);
// 清空数据
vertex.clear();
texCoord.clear();
face.clear();
return geom;
}
/**
* 解析文件数据
* @param assetInfo
* @throws IOException
*/
private void parse(AssetInfo assetInfo) throws IOException {
// 打开文件流
InputStream in = assetInfo.openStream();
Scanner scanner = new Scanner(in);
// 逐行解析文件
String line;
while(scanner.hasNextLine()) {
line = scanner.nextLine();
if (line.startsWith("#")) {
// 跳过注释行
continue;
} else if (line.startsWith("v ")) {
// 解析顶点
line = line.substring(2);
String[] tokens = line.split(",");
float a = Float.valueOf(tokens[0].trim());
float b = Float.valueOf(tokens[1].trim());
float c = Float.valueOf(tokens[2].trim());
// 保存顶点
vertex.add(a);
vertex.add(b);
vertex.add(c);
} else if (line.startsWith("t ")) {
// 解析纹理坐标
line = line.substring(2);
String[] tokens = line.split(",");
float a = Float.valueOf(tokens[0].trim());
float b = Float.valueOf(tokens[1].trim());
// 保存纹理坐标
texCoord.add(a);
texCoord.add(b);
} else if (line.startsWith("f ")) {
// 解析面
line = line.substring(2);
String[] tokens = line.split(",");
int a = Integer.valueOf(tokens[0].trim());
int b = Integer.valueOf(tokens[1].trim());
int c = Integer.valueOf(tokens[2].trim());
// 保存面
face.add(a);
face.add(b);
face.add(c);
}
}
// 关闭文件流
scanner.close();
in.close();
}
/**
* 生成网格
* @return
*/
private Mesh buildMesh() {
// 顶点缓存
int count = vertex.size();
FloatBuffer vb = BufferUtils.createFloatBuffer(count);
for(int i=0; i<count; i++) {
vb.put(vertex.get(i).floatValue());
}
vb.flip();
// 纹理坐标缓存
count = texCoord.size();
FloatBuffer uv = BufferUtils.createFloatBuffer(count);
for(int i=0; i<count; i++) {
uv.put(texCoord.get(i).floatValue());
}
uv.flip();
// 索引缓存
count = face.size();
ShortBuffer ib = BufferUtils.createShortBuffer(count);
for(int i=0; i<count; i++) {
ib.put(face.get(i).shortValue());
}
ib.flip();
// 创建jME3的网格
Mesh mesh = new Mesh();
mesh.setBuffer(Type.Position, 3, vb);
mesh.setBuffer(Type.TexCoord, 2, uv);
mesh.setBuffer(Type.Index, 3, ib);
mesh.setStatic();
mesh.updateCounts();
mesh.updateBound();
return mesh;
}
}
编写测试代码
编写TestYanLoader类,在jME3环境中加载 model.yan
文件,查看该模型。
package net.jmecn.assets;
import com.jme3.app.SimpleApplication;
import com.jme3.asset.ModelKey;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.scene.Spatial;
/**
* 测试加载.yan格式的模型
*
* @author yanmaoyuan
*
*/
public class TestYanLoader extends SimpleApplication {
@Override
public void simpleInitApp() {
// 注册资产路径
assetManager.registerLocator("assets", FileLocator.class);
// 注册解析器
assetManager.registerLoader(YanLoader.class, "yan");
// 加载模型
Spatial model = assetManager.loadAsset(new ModelKey("model.yan"));
rootNode.attachChild(model);
}
public static void main(String[] args) {
TestYanLoader app = new TestYanLoader();
app.start();
}
}
运行结果:
小结
AssetLoader的工作机制并不复杂。主要是通过 openStream 获得输入流,剩下的主要是根据文件的数据结构来进行解析。
通过 registerLoader 方法,可以将某种后缀名的文件与特定 AssetLoader 关联起来。如果你不喜欢使用 AssetManager,也可以单独使用 AssetLoader 类,只要提供一个 AssetInfo 实例即可。
使用泛型 AssetKey 可以加载对应某种类型的资源。
在 AssetLoader 内部,可以通过 AssetInfo 获得 AssetManager 对象,进而加载文件相关联的其他资源。