第十九章:自定义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.txtB.xmlC.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 对象,进而加载文件相关联的其他资源。