<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[jME爱好者]]></title><description><![CDATA[学习，游戏，编程，故事]]></description><link>https://blog.jmecn.net/</link><image><url>http://blog.jmecn.net/favicon.png</url><title>jME爱好者</title><link>https://blog.jmecn.net/</link></image><generator>Ghost 2.9</generator><lastBuildDate>Mon, 06 Apr 2026 10:08:35 GMT</lastBuildDate><atom:link href="https://blog.jmecn.net/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[技术调研]]></title><description><![CDATA[<h2 id="-">关卡制作</h2><p>使用blender制作风格化场景，树、草、地形。</p><p>【中字】虚幻引擎UE4创建风格化场景全流程教学</p><p><a href="https://www.bilibili.com/video/BV1fj41127M7/">https://www.bilibili.com/video/BV1fj41127M7/</a></p><h3 id="--1"> 解决砖块地形贴图重复的问题</h3><p> 通过noise贴图混合两个通道。</p><p><a href="https://www.bilibili.com/video/BV1fj41127M7/">https://www.bilibili.com/video/BV1fj41127M7/</a></p><h2 id="--2">网络服务器</h2><p>kcp 协议 </p><ul><li>kcp-netty <a href="https://github.com/szhnet/kcp-netty">https://github.com/szhnet/kcp-netty</a></li><li>protobuf</li><li>AOI</li><li>服务端寻路：recast生成导航网格，detour 负责寻路</li><li>ECS 服务端使用ECS框架运行，采用20 fps 或者 16fps运行。</li></ul><h3 id="--3"> 服务端导航</h3><p></p><p>客户端，服务器。<a href="https://www.bilibili.com/video/BV1MG4y127B9/">https://www.bilibili.com/video/</a></p>]]></description><link>https://blog.jmecn.net/ji-zhu-diao-yan/</link><guid isPermaLink="false">64feb45cfdc1cd59efaf7cb4</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Mon, 11 Sep 2023 06:41:01 GMT</pubDate><content:encoded><![CDATA[<h2 id="-">关卡制作</h2><p>使用blender制作风格化场景，树、草、地形。</p><p>【中字】虚幻引擎UE4创建风格化场景全流程教学</p><p><a href="https://www.bilibili.com/video/BV1fj41127M7/">https://www.bilibili.com/video/BV1fj41127M7/</a></p><h3 id="--1"> 解决砖块地形贴图重复的问题</h3><p> 通过noise贴图混合两个通道。</p><p><a href="https://www.bilibili.com/video/BV1fj41127M7/">https://www.bilibili.com/video/BV1fj41127M7/</a></p><h2 id="--2">网络服务器</h2><p>kcp 协议 </p><ul><li>kcp-netty <a href="https://github.com/szhnet/kcp-netty">https://github.com/szhnet/kcp-netty</a></li><li>protobuf</li><li>AOI</li><li>服务端寻路：recast生成导航网格，detour 负责寻路</li><li>ECS 服务端使用ECS框架运行，采用20 fps 或者 16fps运行。</li></ul><h3 id="--3"> 服务端导航</h3><p></p><p>客户端，服务器。<a href="https://www.bilibili.com/video/BV1MG4y127B9/">https://www.bilibili.com/video/BV1MG4y127B9/</a></p><p>1、客户端点击地图，产生NavToPoint事件。</p><p>2、客户端执行寻路，并且把该事件发送给服务器。【等待服务器响应】</p><p>3、服务器Controller将请求丢入队列。</p><p>4、服务端GameLoop 遍历 eventQueue，处理队列中的事件。</p><p>5、对应的服务端的System采用与客户端System相同的逻辑。</p><p>6、服务端处理NavToPoint事件时，从请求参数中获取数据，初始化AStarComponent给PlayerEntity。</p><p>7、AStarSystem执行寻路。（1）没找到路径，结束。（2）找到路径，广播给AOI。把玩家实体服务器上的 position、rotation、state、nav信息都发送给客户端。</p><p>8、服务器播动画，客户端也自己播动画。（1）服务器 GameLoop 以16 FPS 执行system。（2）客户端 GameLoop 以 60 FPS 执行system。走到目标点后就改变行走状态。</p><p>9、服务器上寻路停止后，把状态信息也同步给客户端。注意：服务器可以每一帧都把状态同步给客户端，但不必要。</p>]]></content:encoded></item><item><title><![CDATA[第二十二章：Java序列化]]></title><description><![CDATA[<h2 id="-">基本概念</h2><h3 id="--1">什么是序列化</h3><p>Java序列化(Serialization)，即将Java对象转化为二进制的字节数据，反之就是反序列化。</p><h3 id="--2">为什么要序列化</h3><p>序列化后Java对象变成了字节数据，可以更方便地存储和传输。</p><ul><li>当你想要持久化存储数据时（包括文件、数据库、缓存等）</li><li>当你需要通过网络传输对象时（包括RMI、RPC等）</li></ul><h3 id="--3">如何比较序列化</h3><ol><li>序列化后的码流大小：占用网络带宽、存储空间</li><li>序列化的性能：占用CPU、内存</li><li>是否支持跨语言：异构系统的对接和开发语言切换</li><li>API使用的难易度：开发和维护的成本</li></ol><h2 id="jdk-">JDK序列化</h2><p>Java自带的序列化方式，通过 ObjectOutputStream 和 ObjectInputStream 实现。需要使用JDK序列化的类，要在定义时声明 <code>java.io.Serializable</code> 接口或 <code>java.io.Externalizable</code> 接口并生成序列化ID。</p><p><strong>优点：</strong></p><p>这种序列化的主要优点是JDK原生支持，使用起来非常简便。</p><p><strong>缺点：</strong></p><ol><li>无法跨语言。这是Java内置的协议，</li></ol>]]></description><link>https://blog.jmecn.net/java-serialization/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b5c</guid><category><![CDATA[资产管线]]></category><category><![CDATA[Java]]></category><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Fri, 19 Jul 2019 08:23:44 GMT</pubDate><content:encoded><![CDATA[<h2 id="-">基本概念</h2><h3 id="--1">什么是序列化</h3><p>Java序列化(Serialization)，即将Java对象转化为二进制的字节数据，反之就是反序列化。</p><h3 id="--2">为什么要序列化</h3><p>序列化后Java对象变成了字节数据，可以更方便地存储和传输。</p><ul><li>当你想要持久化存储数据时（包括文件、数据库、缓存等）</li><li>当你需要通过网络传输对象时（包括RMI、RPC等）</li></ul><h3 id="--3">如何比较序列化</h3><ol><li>序列化后的码流大小：占用网络带宽、存储空间</li><li>序列化的性能：占用CPU、内存</li><li>是否支持跨语言：异构系统的对接和开发语言切换</li><li>API使用的难易度：开发和维护的成本</li></ol><h2 id="jdk-">JDK序列化</h2><p>Java自带的序列化方式，通过 ObjectOutputStream 和 ObjectInputStream 实现。需要使用JDK序列化的类，要在定义时声明 <code>java.io.Serializable</code> 接口或 <code>java.io.Externalizable</code> 接口并生成序列化ID。</p><p><strong>优点：</strong></p><p>这种序列化的主要优点是JDK原生支持，使用起来非常简便。</p><p><strong>缺点：</strong></p><ol><li>无法跨语言。这是Java内置的协议，其他语言并不了解其编码方式。</li><li>序列化后的数据太长。数据中冗余记录了许多的类型信息，导致有效数据占比很低。</li><li>序列化效率低。由于Java序列化采用同步阻塞IO，相对于目前主流的序列化协议，它的效率非常差。</li></ol><h3 id="serializable">Serializable</h3><p><strong>声明</strong></p><p>首先定义一个类，声明实现 java.io.Serializable 接口。</p><pre><code>package foo.bar;

import java.io.Serializable;

public class Foo implements Serializable {

    private static final long serialVersionUID = -1L;

    private int age;

    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}
</code></pre><p>如果类的成员指向了另一个类的对象，被指向的类也要实现 Serializable 接口，否则就无法序列化。</p><pre><code>package foo.bar;

import java.io.Serializable;

public class Foo implements Serializable {

    private static final long serialVersionUID = -1L;

    private int age;

    private Bar bar;// Foo 中引用了 Bar，Bar也得实现 Serializable 接口

}
</code></pre><h4 id="--4">关键字</h4><p>使用 <code>transient</code> 关键字来标记成员，表示该成员不参与序列化。由于反序列化会忽略 <code>transient</code> 成员，使用时要注意对成员的正确初始化。</p><p>使用 <code>final</code> 关键字标识的对象成员，无论是否使用 <code>transient</code> 关键字，都会参与序列化。<code>final</code> 表示只初始化赋值一次，反序列化相当于一个特殊的构造方法，如果此时不给 <code>final</code> 成员赋值，以后就没有机会赋值了。</p><p>使用 <code>static</code> 关键字标识的成员，属于类的成员，不属于对象的成员，因此不参与对象的序列化。</p><p>例如，下面的 gender、SCORE 就不会被序列化：</p><pre><code>package foo.bar;

import java.io.Serializable;

public class Foo implements Serializable {

    private static final long serialVersionUID = -1L;

    private int age;

    private String name;

    private transient int gender;// transient 标识不参与序列化

    private final int bornYear = 2000;// final 标识的成员会参与序列化

    public final static int SCORE = 10;// static 标识不参与序列化
}
</code></pre><h4 id="serialversionuid">serialVersionUID</h4><p><code>serialVersionUID</code> 是一个特殊的成员，用于判断序列化文件是否已经失效（过期）。</p><p>序列化时这个ID会被编码二进制数据中，反序列化时用来和类中的ID作比较。如果两者不一致，就说明代码被修改过，不可以反序列化。（会抛出 java.io.InvalidClassException 异常）。</p><p>实现了 Serializable 接口的类都要定义一个 <code>serialVersionUID</code> 成员，你可以自己给它赋值，可以让IDE自动计算。</p><p>如果不赋值的话，JVM会根据类的相关信息（包括：类的全名、类中参与序列化的成员、类的方法）自动计算一个ID。每次修改代码，JVM自动计算的ID都会发生变化。</p><p>若是对象序列化后再修改类的代码，有极大几率造成反序列化失败。只加一个 transient 字段是不会有问题的，因为这个成员不参与序列化。但增加方法、减少方法、改属性名、改属性类型都会造成ID变化。</p><p>一般来说，假设代码只是做增量更新，最好不要修改 <code>serialVersionUID</code> 的值。</p><p>注：JDK 中有一个 <code>serialver</code> 工具，可以用来查看类的 serialVersionUID。</p><pre><code>serialver [-classpath 类路径] [-show] [类名称...]
</code></pre><h4 id="--5">序列化与反序列化</h4><p><strong>序列化</strong></p><p>利用ObjectOutputStream对其序列化，然后保存到文件中:</p><pre><code>package foo.bar;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

public class TestSerial {

    private final static String FILENAME = "foo";

    public static void main(String[] args) throws IOException {
        // 创建对象
        Foo obj = new Foo();
        obj.setAge(10);
        obj.setName("tom");

        // 打开文件
        FileOutputStream out = new FileOutputStream(FILENAME);
        ObjectOutputStream oos = new ObjectOutputStream(out);

        // 将对象序列化，并写入文件中
        oos.writeObject(obj);
        oos.flush();

        // 关闭文件
        oos.close();
        out.close();
    }

}
</code></pre><p>利用 ObjectInputStream 执行反序列化：</p><pre><code>package foo.bar;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class TestDeserial {

    private final static String FILENAME = "foo";

    public static void main(String[] args) throws IOException {

        FileInputStream in = new FileInputStream(FILENAME);
        ObjectInputStream ois = new ObjectInputStream(in);

        try {
            Foo obj = (Foo) ois.readObject();
            System.out.printf("age: %d\nname: %s\n", obj.getAge(), obj.getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

        ois.close();
        in.close();
    }

}
</code></pre><p>反序列化时，如果找不到 <code>foo.bar.Foo</code> 这个类会抛出 ClassNotFoundException，如果 serialVersionUID 不一致会抛出 InvalidClassException。</p><h4 id="--6">文件结构分析</h4><p>文件中的二进制数据如下：</p><pre><code>aced 0005 7372 000b 666f 6f2e 6261 722e
466f 6fff ffff ffff ffff ff02 0002 4900
0361 6765 4c00 046e 616d 6574 0012 4c6a
6176 612f 6c61 6e67 2f53 7472 696e 673b
7870 0000 000a 7400 0374 6f6d 
</code></pre><p>上述的内容分为以下几部分：</p><p><strong>第一部分是JDK序列化的魔数</strong></p><p>前4个字节是固定的，表示这是通过JDK序列化的数据。</p><ul><li><code>aced</code> STREAM_MAGIC，声明使用了序列化协议</li><li><code>0005</code> STREAM_VERSION，声明JDK序列化协议的版本</li></ul><p><em>注意：JDK序列化时，数字的编码采用大端格式(Big Endian)。版本号 “5” 编码为 “0005”，而不是 “0500”。</em></p><p><strong>第二部分用于描述对象的类型信息</strong></p><ul><li><code>73</code> TC_OBJECT 表示序列化的是一个Java对象</li><li><code>72</code> TC_CLASSDESC 表示后面是对象的类型信息</li><li><code>000b</code> 表示类名的长度，即 11 字节</li><li><code>66 6f6f 2e62 6172 2e46 6f</code> 接下来11字节是类名，即 "foo.bar.Foo"</li><li><code>ff ffff ffff ffff ff</code> 类名后的8字节是一个长整数，即 serialVersionUID = -1L。</li><li><code>02</code> SC_SERIALIZABLE 标识位，说明这个类实现了Serializable接口。</li></ul><p><strong>第三部分是对象的字段表</strong></p><ul><li><code>0002</code> 表示这个对象中有2个属性。</li><li><code>49</code> 即 I 表示 int，说明这是一个32位整数。</li><li><code>00 03</code> 表示属性名的长度，即 3 字节</li><li><code>61 6765</code> 即 "age"</li><li><code>4c</code> 即 L，表示引用类型，说明这个属性是某个类型的引用。</li><li><code>00 04</code> 表示属性名的长度，即 4 字节</li><li><code>6e 616d 65</code> 即 "name"</li><li><code>74</code> TC_STRING 表示后面是个字符串</li><li><code>0012</code> 表示字符串长度，即 18 字节</li><li><code>4c6a 6176 612f 6c61 6e67 2f53 7472 696e 673b</code> 即"Ljava/lang/String;"</li></ul><p><strong>第四部分是父类的描述信息</strong></p><ul><li><code>78</code> TC_ENDBLOCKDATA 表示这个数据块到此结束。</li><li><code>70</code> TC_NULL 表示这个类没有父类</li></ul><p>如果 foo.bar.Foo 有父类，那么第四部分将重复第二部分的结构，描述父类的信息。</p><p><strong>第五部分是对象的属性值</strong></p><p>根据第三部分的字段表，可以知道该如何识别属性的值。</p><p>第一个属性age，根据字段表知道它是一个32位的整数，</p><ul><li><code>0000 000a</code> 即 10。</li></ul><p>第二个属性name，根据类型知道它是一个字符串</p><ul><li><code>74</code> TC_STRING 表示后面是个字符串</li><li><code>00 03</code> 表示字符串长度为3</li><li><code>74 6f6d</code> 即 "tom"</li></ul><h3 id="externalizable">Externalizable</h3><p>java.io.Externalizable 继承于 java.io.Serializable 接口。它不会记录对象的字段表，允许程序员自定义对象的编码方式。</p><pre><code>package foo.bar;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;

public class Bar implements Externalizable {

    private static final long serialVersionUID = -1L;

    private int age;

    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeInt(age);
        out.writeUTF(name);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        age = in.readInt();
        name = in.readUTF();
    }

}
</code></pre><h4 id="--7">数据结构</h4><p>二进制文件中的数据内容：</p><pre><code>aced 0005 7372 000b 666f 6f2e 6261 722e
4261 72ff ffff ffff ffff ff0c 0000 7870
7709 0000 000a 0003 746f 6d78 
</code></pre><p><strong>第一部分是JDK序列化的魔数</strong></p><p>前4个字节是固定的，表示这是通过JDK序列化的数据。</p><ul><li><code>aced</code> STREAM_MAGIC，声明使用了序列化协议</li><li><code>0005</code> STREAM_VERSION，声明JDK序列化协议的版本</li></ul><p><strong>第二部分用于描述对象的类型信息</strong></p><ul><li><code>73</code> TC_OBJECT 表示序列化的是一个Java对象</li><li><code>72</code> TC_CLASSDESC 表示后面是对象的类型信息</li><li><code>000b</code> 表示类名的长度，即 11 字节</li><li><code>666f 6f2e 6261 722e 4261 72</code> 接下来11字节是类名，即 "foo.bar.Bar"</li><li><code>ff ffff ffff ffff ff</code> 类名后的8字节是一个长整数，即 serialVersionUID = -1L。</li><li><code>0c</code> SC_EXTERNALIZABLE | SC_BLOCK_DATA 标识位，说明这个类实现了Externalizable接口，并且使用块数据编码。</li></ul><p><strong>第三部分字段表</strong></p><ul><li><code>0000</code> 字段表长度为0，字段表被省掉了。</li></ul><p><strong>第四部分父类信息</strong></p><ul><li><code>78</code> TC_ENDBLOCKDATA 表示这个数据块到此结束。</li><li><code>70</code> TC_NULL 表示这个类没有父类</li></ul><p><strong>第五部分数据值</strong></p><p>由于第三部分的字段表没有了，因此二进制数据的字段顺序、字段的数据类型、字段的长度，完全靠代码中的 <code>writeExternal</code> 和 <code>readExternal</code> 方法来指定。</p><ul><li><code>0000 000a</code> 这是一个整数，即10。</li><li><code>0003 746f 6d78</code> 这是一个长度3字节的UTF8字符串，即"tom"。</li></ul><h2 id="xml">XML</h2><p>XML（Extensible Markup Language）是一种常用的序列化和反序列化协议， 它历史悠久，从1998年的1.0版本被广泛使用至今。</p><p><strong>优点</strong></p><ol><li>人机可读性好</li><li>可指定元素或特性的名称</li></ol><p><strong>缺点</strong></p><ol><li>序列化数据只包含数据本身以及类的结构，不包括类型标识和程序集信息。</li><li>只能序列化公共属性和字段</li><li>不能序列化方法/接口</li><li>文件庞大，文件格式复杂，传输占带宽</li></ol><p><strong>使用场景</strong></p><ol><li>当做配置文件存储数据</li><li>WebService，基于 SOAP 协议。</li></ol><h3 id="jackson">Jackson</h3><p>下面使用 Jackson 的 jackson-dataformat-xml 模块作为序列化工具。</p><pre><code>&lt;!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-xml --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.fasterxml.jackson.dataformat&lt;/groupId&gt;
    &lt;artifactId&gt;jackson-dataformat-xml&lt;/artifactId&gt;
    &lt;version&gt;2.9.9&lt;/version&gt;
&lt;/dependency&gt;
</code></pre><p>序列化</p><pre><code>package foo.bar;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

import java.io.FileOutputStream;
import java.io.IOException;

public class TestSerXml {

    private final static String FILENAME = "foo.xml";

    public static void main(String[] args) throws IOException {
        // 创建对象
        Foo obj = new Foo();
        obj.setAge(10);
        obj.setName("tom");

        // 打开文件
        FileOutputStream out = new FileOutputStream(FILENAME);

        // 序列化
        XmlMapper xmlMapper = new XmlMapper();
        xmlMapper.writeValue(out, obj);

        // 关闭文件
        out.close();
    }

}
</code></pre><p>反序列化</p><pre><code>package foo.bar;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;

import java.io.FileInputStream;
import java.io.IOException;

public class TestDeserXml {

    private final static String FILENAME = "foo.xml";

    public static void main(String[] args) throws IOException {

        // 打开文件
        FileInputStream in = new FileInputStream(FILENAME);

        // 反序列化
        XmlMapper xmlMapper = new XmlMapper();
        Foo obj = xmlMapper.readValue(in, Foo.class);
        System.out.printf("age: %d\nname: %s\n", obj.getAge(), obj.getName());

        // 关闭文件
        in.close();
    }

}
</code></pre><p>文件中的数据如下:</p><pre><code>&lt;Foo&gt;&lt;age&gt;10&lt;/age&gt;&lt;name&gt;tom&lt;/name&gt;&lt;/Foo&gt;
</code></pre><h2 id="json">JSON</h2><p>JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式。它基于 ECMAScript (w3c制定的js规范)的一个子集， JSON采用与编程语言无关的文本格式，但是也使用了类C语言（包括C， C++， C#， Java， JavaScript， Perl， Python等）的习惯，简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。</p><p><strong>优点：</strong></p><ol><li>前后兼容性高</li><li>数据格式比较简单，易于读写</li><li>序列化后数据较小，可扩展性好，兼容性好</li><li>与XML相比，其协议比较简单，解析速度比较快</li></ol><p><strong>缺点：</strong></p><ol><li>数据的描述性比XML差</li><li>不适合性能要求为ms级别的情况</li><li>额外空间开销比较大</li></ol><p><strong>使用场景（可替代XML）</strong></p><ol><li>跨防火墙访问</li><li>可调式性要求高的情况</li><li>基于Web browser的Ajax请求，以及Mobile app与服务端之间的通讯，</li><li>传输数据量相对小，实时性要求相对低（例如秒级别）的服务</li><li>以<code>动态类型</code>语言为主的系统</li></ol><h3 id="fastjson">Fastjson</h3><p>Jackson也常用于对json进行序列化。但本文选择使用alibaba的fastjson作为序列化工具。</p><pre><code>&lt;!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.alibaba&lt;/groupId&gt;
    &lt;artifactId&gt;fastjson&lt;/artifactId&gt;
    &lt;version&gt;1.2.58&lt;/version&gt;
&lt;/dependency&gt;
</code></pre><p>序列化</p><pre><code>package foo.bar;
import com.alibaba.fastjson.JSON;

import java.io.FileOutputStream;
import java.io.IOException;

public class TestSerJson {

    private final static String FILENAME = "foo.json";

    public static void main(String[] args) throws IOException {
        // 创建对象
        Foo obj = new Foo();
        obj.setAge(10);
        obj.setName("tom");

        // 打开文件
        FileOutputStream out = new FileOutputStream(FILENAME);

        // 序列化
        JSON.writeJSONString(out, obj);

        // 关闭文件
        out.close();
    }

}
</code></pre><p>反序列化</p><pre><code>package foo.bar;
import com.alibaba.fastjson.JSON;

import java.io.FileInputStream;
import java.io.IOException;

public class TestDeserJson {

    private final static String FILENAME = "foo.json";

    public static void main(String[] args) throws IOException {

        // 打开文件
        FileInputStream in = new FileInputStream(FILENAME);

        // 反序列化
        Foo obj = JSON.parseObject(in, Foo.class);
        System.out.printf("age: %d\nname: %s\n", obj.getAge(), obj.getName());

        // 关闭文件
        in.close();
    }

}
</code></pre><p>文件中的数据如下：</p><pre><code>{"age":10,"name":"tom"}
</code></pre><h2 id="protobuf">protobuf</h2><p>Protobuf是google开源的项目，全称 Google Protocol Buffers。它将数据结构以.proto文件进行描述，通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。</p><p>protostuff 基于protobuf协议，但不需要配置proto文件，直接导包即可使用。</p><p><strong>优点：</strong></p><ol><li>结构化数据存储格式（如同xml,json等）</li><li>高性能编解码技术</li><li>语言和平台无关，扩展性好</li><li>支持C++,C#,Dart,Go,Java,JavaScript,Objective-C,PHP,Python,Ruby语言。</li><li>通过标识字段的顺序，可以实现协议的前向兼容</li></ol><p><strong>缺点：</strong></p><ul><li>需要额外定义 <code>.proto</code> 文件来描述数据结构，使用较为复杂</li><li>需要依赖于工具生成代码</li></ul><p><strong>适用场景：</strong></p><ul><li>对性能要求高的RPC调用</li><li>具有良好的跨防火墙的访问属性</li><li>适合应用层对象的持久化</li><li>静态类型语言</li></ul><h3 id="--8">例子</h3><p>首先创建Foo.proto文件，定义Foo的数据结构。</p><pre><code>syntax = "proto3";
package foo.bar;

message Foo {
    int32 age = 1;
    string name = 2;
}
</code></pre><p>使用 protoc 工具生成Java代码。这个工具需要去 Google 的开发者平台下载，或者下载源码编译。</p><ul><li><a href="https://developers.google.cn/protocol-buffers/">Google官网: protocol-buffers</a></li><li><a href="https://github.com/protocolbuffers/protobuf">Github: protocolbuffers/protobuf</a></li></ul><p>生成代码的指令如下：</p><pre><code>protoc --java_out src/mava/java Foo.proto
</code></pre><p>protoc 编译器会生成了一个.java文件，以及一个特殊的Builder类（该类是用来创建消息类接口的），用于对Foo类进行序列化和反序列化。</p><p>这个代码很长，就不在文章中展示了。默认生成的类名是 <code>FooOuterClass</code>，可以在 Foo.proto 文件中配置生成类的包名和类名。</p><pre><code>syntax = "proto3";
package foo.bar;

option java_package = "foo.bar.builder";
option java_outer_classname = "FooBuilder";

message Foo {
    int32 age = 1;
    string name = 2;
}
</code></pre><p>在Java中使用protobuff，需要依赖protobuf-java模块。</p><pre><code>&lt;!-- https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java --&gt;
&lt;dependency&gt;
    &lt;groupId&gt;com.google.protobuf&lt;/groupId&gt;
    &lt;artifactId&gt;protobuf-java&lt;/artifactId&gt;
    &lt;version&gt;3.9.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre><p>序列化</p><pre><code>package foo.bar;
import java.io.FileOutputStream;
import java.io.IOException;

import foo.bar.FooOuterClass.Foo;

public class TestSerProto {
    
    private final static String FILENAME = "foo.pb";

    public static void main(String[] args) throws IOException {
        // 创建对象
        Foo obj = Foo.newBuilder()
            .setAge(10)
            .setName("tom")
            .build();

        // 打开文件
        FileOutputStream out = new FileOutputStream(FILENAME);

        // 序列化
        obj.writeTo(out);

        // 关闭文件
        out.close();
    }

}
</code></pre><p>反序列化</p><pre><code>package foo.bar;
import java.io.FileInputStream;
import java.io.IOException;

import foo.bar.FooOuterClass.Foo;

public class TestDeserProto {

    private final static String FILENAME = "foo.pb";

    public static void main(String[] args) throws IOException {

        // 打开文件
        FileInputStream in = new FileInputStream(FILENAME);

        // 反序列化
        Foo obj = Foo.parseFrom(in);
        System.out.printf("age: %d\nname: %s\n", obj.getAge(), obj.getName());

        // 关闭文件
        in.close();
    }

}
</code></pre><p>文件中的数据如下：</p><pre><code>080a 1203 746f 6d
</code></pre><ul><li>080a 表示整数 10</li><li>1203 746f 6d 表示长度为3字节的字符串，即 "tom"</li></ul><h2 id="--9">结束</h2><p>还有很多中序列化协议，本文没有涉及。不同序列化方式的对比，可参考 Github:jvm-serilizers项目的可视化分析。</p><p><a href="https://github.com/eishay/jvm-serializers/wiki">https://github.com/eishay/jvm-serializers/wiki</a></p>]]></content:encoded></item><item><title><![CDATA[第二十一章：资产配置文件]]></title><description><![CDATA[<p>jME3允许开发者通过资产配置文件（AssetConfig）来管理 AssetLocator 和 AssetLoader 。本文将介绍 AssetConfig 的格式及用法。</p>
<h3 id="">繁琐的代码</h3>
<p><a href="http://blog.jmecn.net/multi-media-asset-pipeline/">第一章：多媒体资产管道</a> 中列出了 jME3 支持的各种资产文件格式，每种格式都有对应的 <code>AssetLoader</code> 来进行解析。根据前几章的介绍，如果想让 <code>AssetManager</code> 能够加载这些文件，需要先调用 <code>registerLoader</code> 方法配置加载器。</p>
<p>下面的代码演示了如何配置这些 <code>AssetLoader</code>。 注意，有些文件在PC和Android系统中需要使用不同的类来加载。</p>
<pre><code>package net.jmecn.assets;

import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.AndroidLocator;
import com.jme3.asset.plugins.ClasspathLocator;</code></pre>]]></description><link>https://blog.jmecn.net/asset-config/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b4f</guid><category><![CDATA[资产管线]]></category><category><![CDATA[jMonkeyEngine]]></category><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Mon, 15 Jan 2018 06:02:40 GMT</pubDate><content:encoded><![CDATA[<p>jME3允许开发者通过资产配置文件（AssetConfig）来管理 AssetLocator 和 AssetLoader 。本文将介绍 AssetConfig 的格式及用法。</p>
<h3 id="">繁琐的代码</h3>
<p><a href="http://blog.jmecn.net/multi-media-asset-pipeline/">第一章：多媒体资产管道</a> 中列出了 jME3 支持的各种资产文件格式，每种格式都有对应的 <code>AssetLoader</code> 来进行解析。根据前几章的介绍，如果想让 <code>AssetManager</code> 能够加载这些文件，需要先调用 <code>registerLoader</code> 方法配置加载器。</p>
<p>下面的代码演示了如何配置这些 <code>AssetLoader</code>。 注意，有些文件在PC和Android系统中需要使用不同的类来加载。</p>
<pre><code>package net.jmecn.assets;

import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.AndroidLocator;
import com.jme3.asset.plugins.ClasspathLocator;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.audio.plugins.NativeVorbisLoader;
import com.jme3.audio.plugins.OGGLoader;
import com.jme3.audio.plugins.WAVLoader;
import com.jme3.cursors.plugins.CursorLoader;
import com.jme3.export.binary.BinaryLoader;
import com.jme3.font.plugins.BitmapFontLoader;
import com.jme3.material.plugins.J3MLoader;
import com.jme3.scene.plugins.MTLLoader;
import com.jme3.scene.plugins.OBJLoader;
import com.jme3.scene.plugins.blender.BlenderLoader;
import com.jme3.scene.plugins.fbx.FbxLoader;
import com.jme3.scene.plugins.gltf.BinLoader;
import com.jme3.scene.plugins.gltf.GlbLoader;
import com.jme3.scene.plugins.gltf.GltfLoader;
import com.jme3.scene.plugins.ogre.MaterialLoader;
import com.jme3.scene.plugins.ogre.MeshLoader;
import com.jme3.scene.plugins.ogre.SceneLoader;
import com.jme3.scene.plugins.ogre.SkeletonLoader;
import com.jme3.shader.plugins.GLSLLoader;
import com.jme3.system.JmeSystem;
import com.jme3.texture.plugins.AWTLoader;
import com.jme3.texture.plugins.AndroidNativeImageLoader;
import com.jme3.texture.plugins.DDSLoader;
import com.jme3.texture.plugins.HDRLoader;
import com.jme3.texture.plugins.TGALoader;

public class TestAssetConfig {

    public static void main(String[] args) {

        AssetManager assetManager = JmeSystem.newAssetManager();

        /**
         * 注册资产定位器
         */
        assetManager.registerLocator(&quot;assets&quot;, FileLocator.class);
        assetManager.registerLocator(&quot;/&quot;, ClasspathLocator.class);
        // Android系统应该使用下面的定位器，依赖 jme3-android-{version}.jar
        // assetManager.registerLocator(&quot;/&quot;, AndroidLocator.class);

        /**
         * 注册资产加载器
         */
        
        /* 模型&amp;材质 */
        assetManager.registerLoader(BinaryLoader.class, &quot;j3o&quot;);
        assetManager.registerLoader(J3MLoader.class, &quot;j3m&quot;);
        assetManager.registerLoader(J3MLoader.class, &quot;j3md&quot;);
        
        assetManager.registerLoader(OBJLoader.class, &quot;obj&quot;);
        assetManager.registerLoader(MTLLoader.class, &quot;mtl&quot;);
        
        // Blender 格式，依赖jme3-blender-{version}.jar。{version} &gt;= 3.0
        assetManager.registerLoader(BlenderLoader.class, &quot;blend&quot;);
        // Orge 格式，依赖jme3-plugins-{version}.jar。 {version} &gt;= 3.0
        assetManager.registerLoader(MeshLoader.class, &quot;meshxml&quot;, &quot;mesh.xml&quot;);
        assetManager.registerLoader(SkeletonLoader.class, &quot;skeletonxml&quot;, &quot;skeleton.xml&quot;);
        assetManager.registerLoader(MaterialLoader.class, &quot;material&quot;);
        assetManager.registerLoader(SceneLoader.class, &quot;scene&quot;);
        // FBX 格式，依赖 jme3-plugins-{version}.jar。{version} &gt;= 3.1.0-stable
        assetManager.registerLoader(FbxLoader.class, &quot;fbx&quot;);
        // glTF 格式，依赖jme3-plugins-{version}.jar。 {version} &gt;= 3.2.0-stable
        assetManager.registerLoader(GltfLoader.class, &quot;gltf&quot;);
        assetManager.registerLoader(BinLoader.class, &quot;bin&quot;);
        assetManager.registerLoader(GlbLoader.class, &quot;glb&quot;);
        
        /* 着色器 */
        assetManager.registerLoader(GLSLLoader.class, &quot;vert&quot;, &quot;frag&quot;, &quot;geom&quot;, &quot;tsctrl&quot;, &quot;tseval&quot;, &quot;glsl&quot;, &quot;glsllib&quot;);
        
        /* 音频文件 */
        assetManager.registerLoader(WAVLoader.class, &quot;wav&quot;);
        // Ogg格式，依赖jme3-jogg-{version}.jar。{version} &gt;= 3.0
        assetManager.registerLoader(OGGLoader.class, &quot;ogg&quot;);
        // Android系统，依赖jme3-android-{version.jar}。{version} &gt;= 3.0
        // assetManager.registerLoader(NativeVorbisLoader.class, &quot;ogg&quot;);
        
        /* 图片文件 */
        assetManager.registerLoader(TGALoader.class, &quot;tga&quot;);
        assetManager.registerLoader(DDSLoader.class, &quot;dds&quot;);
        assetManager.registerLoader(HDRLoader.class, &quot;hdr&quot;);
        // java.awt 只在桌面平台有效，依赖 jme3-desktop-{version}.jar。{version} &gt;= 3.0
        assetManager.registerLoader(AWTLoader.class, &quot;jpg&quot;, &quot;bmp&quot;, &quot;gif&quot;, &quot;png&quot;, &quot;jpeg&quot;);
        // Android 系统与桌面不同，依赖 jme3-android-{version}.jar。{version} &gt;= 3.0
        // assetManager.registerLoader(AndroidNativeImageLoader.class, &quot;jpg&quot;, &quot;bmp&quot;, &quot;gif&quot;, &quot;png&quot;, &quot;jpeg&quot;);
        
        /* 鼠标光标 */
        // 鼠标只有桌面平台有效，依赖 jme3-desktop-{version}.jar。{version} &gt;= 3.0
        assetManager.registerLoader(CursorLoader.class, &quot;ico&quot;, &quot;ani&quot;, &quot;cur&quot;);
        
        /* 字体 */
        assetManager.registerLoader(BitmapFontLoader.class, &quot;fnt&quot;);
    }

}
</code></pre>
<h3 id="">默认资产配置</h3>
<p>上面的代码非常繁琐，jME3允许开发者通过资产配置文件（AssetConfig）来管理 <code>AssetLocator</code> 和 <code>AssetLoader</code> 。</p>
<p>下面是 <code>jme3-core.jar</code> 中内置的“通用资产配置” <a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/main/resources/com/jme3/asset/General.cfg"><code>General.cfg</code></a> ：</p>
<pre><code># Generic locators that should be supported on all platforms.
LOCATOR / com.jme3.asset.plugins.ClasspathLocator

# Generic loaders that should be supported on all platforms.
LOADER com.jme3.audio.plugins.WAVLoader : wav
LOADER com.jme3.cursors.plugins.CursorLoader : ani, cur, ico
LOADER com.jme3.material.plugins.J3MLoader : j3m
LOADER com.jme3.material.plugins.J3MLoader : j3md
LOADER com.jme3.material.plugins.ShaderNodeDefinitionLoader : j3sn
LOADER com.jme3.font.plugins.BitmapFontLoader : fnt
LOADER com.jme3.texture.plugins.DDSLoader : dds
LOADER com.jme3.texture.plugins.PFMLoader : pfm
LOADER com.jme3.texture.plugins.HDRLoader : hdr
LOADER com.jme3.texture.plugins.TGALoader : tga
LOADER com.jme3.export.binary.BinaryLoader : j3o
LOADER com.jme3.export.binary.BinaryLoader : j3f
LOADER com.jme3.scene.plugins.OBJLoader : obj
LOADER com.jme3.scene.plugins.MTLLoader : mtl
LOADER com.jme3.scene.plugins.ogre.MeshLoader : meshxml, mesh.xml
LOADER com.jme3.scene.plugins.ogre.SkeletonLoader : skeletonxml, skeleton.xml
LOADER com.jme3.scene.plugins.ogre.MaterialLoader : material
LOADER com.jme3.scene.plugins.ogre.SceneLoader : scene
LOADER com.jme3.scene.plugins.blender.BlenderLoader : blend
LOADER com.jme3.shader.plugins.GLSLLoader : vert, frag, geom, tsctrl, tseval, glsl, glsllib
LOADER com.jme3.scene.plugins.fbx.FbxLoader : fbx
LOADER com.jme3.scene.plugins.gltf.GltfLoader : gltf
LOADER com.jme3.scene.plugins.gltf.BinLoader : bin
LOADER com.jme3.scene.plugins.gltf.GlbLoader : glb
</code></pre>
<p>PC 和 Android 由于各自系统的特点，分别有不同的配置。</p>
<p><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/main/resources/com/jme3/asset/Desktop.cfg"><code>Desktop.cfg</code></a>:</p>
<pre><code>INCLUDE com/jme3/asset/General.cfg

# Desktop-specific loaders
LOADER com.jme3.texture.plugins.AWTLoader : jpg, bmp, gif, png, jpeg
LOADER com.jme3.audio.plugins.OGGLoader : ogg
</code></pre>
<p><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-android/src/main/resources/com/jme3/asset/Android.cfg"><code>Android.cfg</code></a>:</p>
<pre><code>INCLUDE com/jme3/asset/General.cfg

# Android specific locators
LOCATOR / com.jme3.asset.plugins.AndroidLocator

# Android specific loaders
LOADER com.jme3.texture.plugins.AndroidNativeImageLoader : jpg, bmp, gif, png, jpeg
LOADER com.jme3.audio.plugins.NativeVorbisLoader : ogg
</code></pre>
<p>jME3 应用程序启动时会自动读取上述的配置文件，因此开发者才可以直接使用 <code>AssetManager</code> 来加载各种资产。</p>
<h3 id="">配置文件的语法</h3>
<p>基本规则：</p>
<ul>
<li><code>AssetConfig</code> 文件是纯文本文件，后缀名为 <code>.cfg</code>；</li>
<li>文件逐行解析，每行执行一条命令；</li>
<li>空行将被忽略；</li>
<li>文件中的字符串不需要引号；</li>
</ul>
<h4 id="">注释</h4>
<p>语法：<code># {contentx}</code></p>
<p>说明：井号开头的行是注释，解析时将会被忽略。</p>
<p>例子：<code># Desktop specific loaders</code></p>
<h4 id="">包含</h4>
<p>语法：<code>INCLUDE {url}</code></p>
<p>说明：包含另一个配置文件，<code>{url}</code> 是该文件的路径。</p>
<p>例子：<code>INCLUDE com/jme3/asset/General.cfg</code></p>
<p>注意，<code>INCLUDE</code> 命令并不需要写在开头，它可以出现在文件中的任意位置。</p>
<h4 id="">注册定位器</h4>
<p>语法：<code>LOCATOR {rootPath} {AssetLocatorClass}</code></p>
<p>说明：注册一个资产定位器。 <code>{rootPath}</code> 表示资产根目录，<code>{AssetLocatorClass}</code> 表示对应 AssetLoader 类的全名。</p>
<p>这一行等同于执行下列语句：</p>
<pre><code>assetManager.registerLocator(&quot;{rootPath}&quot;, {AssetLocatorClass});
</code></pre>
<p>例子：<code>LOCATOR / com.jme3.asset.plugins.ClasspathLocator</code></p>
<h4 id="">注册加载器</h4>
<p>语法：</p>
<ul>
<li><code>LOADER {AssetLoaderClass} : {ext}</code></li>
<li><code>LOADER {AssetLoaderClass} : {ext1}, {ext2}, {ext3}</code></li>
</ul>
<p>说明：注册一个资产加载器。<code>{AssetLoaderClass}</code> 表示对应 AssetLoader 类的全名，<code>{ext}</code> 表示文件后缀名；多个后缀名之间用逗号 (,) 隔开。</p>
<p>这一行等同于执行下一行语句：</p>
<pre><code>assetManager.registerLoader({AssetLoaderClass}, {ext});
</code></pre>
<p>或：</p>
<pre><code>assetManager.registerLoader({AssetLoaderClass}, {ext1}, {ext2}, {ext3});
</code></pre>
<p>例子：</p>
<ul>
<li><code>LOADER com.jme3.export.binary.BinaryLoader : j3o</code></li>
<li><code>LOADER com.jme3.shader.plugins.GLSLLoader : vert, frag, geom, tsctrl, tseval, glsl, glsllib</code></li>
</ul>
<h3 id="">自定义配置文件</h3>
<p>在工程的 <code>net.jmecn.assets</code> 包中创建 <code>MyAssetConfig.cfg</code> 文件，内容如下：</p>
<pre><code># 配置AssetLocator
LOCATOR sevenzipjbinding-9.20-2.00beta-AllWindows.zip net.jmecn.assets.SevenZAssetLocator

# 配置AssetLoader
LOADER net.jmecn.assets.TextLoader : txt

# 包含内置桌面配置文件
INCLUDE com/jme3/asset/Desktop.cfg
</code></pre>
<p>这个配置文件使用了前几章开发的 <code>SevenZAssetLocator</code> 和 <code>TextLoader</code> 类，并包含了 jME3 系统自带的 <code>Desktop.cfg</code>。</p>
<h4 id="">独立使用</h4>
<p>下面演示如何通过配置文件来创建 AssetManager 。</p>
<p>代码如下：</p>
<pre><code>package net.jmecn.assets;

import java.net.URL;

import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetManager;
import com.jme3.system.JmeSystem;

/**
 * 测试自定义配置文件
 * @author yanmaoyuan
 *
 */
public class TestCustomConfig {
    public static void main(String[] args) {
        // 获得配置文件的URL
        URL url = TestCustomConfig.class.getResource(&quot;net/jmecn/assets/MyAssetConfig.cfg&quot;);
        // 使用URL来创建AssetManager
        AssetManager assetManager = JmeSystem.newAssetManager(url);//&lt;--- 注意这里
        
        String str = assetManager.loadAsset(new AssetKey&lt;String&gt;(&quot;sevenzipjbinding-9.20-2.00beta-AllWindows/ReleaseNotes.txt&quot;));
        System.out.println(str);
    }
}
</code></pre>
<p>关键代码就一行： <code>JmeSystem.newAssetManager(url)</code>。</p>
<p>运行结果如下：</p>
<pre><code>一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.ogre.MeshLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.ogre.SkeletonLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.ogre.MaterialLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.ogre.SceneLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.blender.BlenderModelLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.fbx.FbxLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.gltf.GltfLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.gltf.BinLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.gltf.GlbLoader
一月 15, 2018 12:15:16 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.audio.plugins.OGGLoader
7-Zip-JBinding Release Notes
============================

Main features of 9.20-2.00beta (Release candidate, extraction/compression/update, cross-platform, based on zip/p7zip 9.20)

    * Extraction of
        - 7z, Arj, BZip2, Cab, Chm, Cpio, Deb, GZip, HFS, Iso, Lzh,
          Lzma, Nsis, Rar, Rpm, Split, Tar, Udf, Wim, Xar, Z, Zip

        - Archive format auto detection
        - Support for password protected and volumed archives
        - Simple extraction interface

    * Compression &amp; update of
        - 7z, Zip, Tar, GZip, BZip2
        - Archive format specific or generic compression API
        
        * 7-Zip-JBinding requires Java 1.5 or higher

        * Cross-platform. Binaries available for
            - Windows 32/64
            - Linux 32/64
            - Mac OS X 32/64
            - ARM (ASMv4+, small endian)

        * Multi-platform distributions (with platform auto-detection):
            - AllWindows - includes Win32 and Win64
            - AllLinux - includes Linux32 and Linux64 (ARM not included)
            - AllMax - includes Mac OS X 32 and Mac OS X 64
            - AllPlatforms - includes AllWindows, AllLinux and AllMac + build optimized for RaspberryPI

    * JavaDoc + Snippets (see documentation on the web: sevenzipjbind.sf.net)

        * Over 6900 JUnit tests:
            - 7z, Zip, Tar, Rar, Lzma, Iso, GZip, Cpio, BZIP2,
              Z, Arj, Lzh, Cab, Chm, Nsis, DEB, RPM, UDF

    * Available on the Maven Central (a week after the official release)
        &lt;dependency&gt;
            &lt;groupId&gt;net.sf.sevenzipjbinding&lt;/groupId&gt;
            &lt;artifactId&gt;sevenzipjbinding&lt;/artifactId&gt;
            &lt;version&gt;9.20-2.00beta&lt;/version&gt;
        &lt;/dependency&gt;
        &lt;dependency&gt;
            &lt;groupId&gt;net.sf.sevenzipjbinding&lt;/groupId&gt;
            &lt;artifactId&gt;sevenzipjbinding-all-platforms&lt;/artifactId&gt;
            &lt;version&gt;9.20-2.00beta&lt;/version&gt;
        &lt;/dependency&gt;
</code></pre>
<p>可以看到，<code>ReleaseNodes.txt</code> 的内容被打印出来了，但正文的前面部分有很多警告信息。</p>
<pre><code>一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.ogre.MeshLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.ogre.SkeletonLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.ogre.MaterialLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.ogre.SceneLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.blender.BlenderModelLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.fbx.FbxLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.gltf.GltfLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.gltf.BinLoader
一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.gltf.GlbLoader
一月 15, 2018 12:15:16 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.audio.plugins.OGGLoader
</code></pre>
<p>AssetManager 在解析配置文件时，会根据类名去查找对应的 AssetLoader 类，并使用Java的反射机制来装载它。如果找不到这个类，就会在日志中打印出警告信息。例如：</p>
<pre><code>一月 15, 2018 12:15:15 下午 com.jme3.asset.AssetConfig loadText
警告: Cannot find loader com.jme3.scene.plugins.ogre.MeshLoader
</code></pre>
<p>对于 <code>AssetManager</code> 来说，<strong>这并不是一个错误</strong>。AssetManager 会忽略这一行，继续执行配置文件中的其他的命令。</p>
<p>在本文的第一节已经介绍过，<code>MeshLoader</code>、<code>GltfLoader</code>、<code>FbxLoader</code> 等类，都依赖 <code>jme3-plugins-{version}.jar</code>；<code>OGGLoader</code> 依赖 <code>jme3-jogg-{version}.jar</code>。如果项目的依赖中缺少对应的jar包，就无法解析对应格式的资产文件。</p>
<h4 id="simpleapplication">在SimpleApplication中使用</h4>
<p>如果你想在 SimpleApplication 的子类中使用自定义资产配置，可以通过 AppSettings 来进行设置。</p>
<pre><code>package net.jmecn.assets;

import java.net.URL;

import com.jme3.app.SimpleApplication;
import com.jme3.asset.AssetKey;
import com.jme3.system.AppSettings;

/**
 * 测试自定义配置文件
 * 
 * @author yanmaoyuan
 *
 */
public class TestCustomConfig extends SimpleApplication {
    public static void main(String[] args) {
        URL url = Thread.currentThread().getContextClassLoader()
                .getResource(&quot;net/jmecn/assets/MyAssetConfig.cfg&quot;);
        AppSettings settings = new AppSettings(true);
        settings.put(&quot;AssetConfigURL&quot;, url.toString());

        TestCustomConfig app = new TestCustomConfig();
        app.setSettings(settings);
        app.start();
    }

    @Override
    public void simpleInitApp() {
        AssetKey&lt;String&gt; key = new AssetKey&lt;String&gt;(
                &quot;sevenzipjbinding-9.20-2.00beta-AllWindows/ReleaseNotes.txt&quot;);
        String str = assetManager.loadAsset(key);
        System.out.println(str);
    }
}
</code></pre>
<p>运行结果是一样的。</p>
]]></content:encoded></item><item><title><![CDATA[第二十章：资产缓存]]></title><description><![CDATA[<p>在 jMonkeyEngine 中，资产缓存（AssetCache）对开发者是个黑盒。绝大多数时间你都不会意识到它的存在，直到有一天程序突然发生内存溢出，抛出一个 <code>OutOfMemoryError</code>。</p>
<p>例如：</p>
<pre><code>Exception in thread &quot;main&quot; java.lang.OutOfMemoryError: Java heap space
</code></pre>
<p>众所周知，Java程序运行于虚拟机（JVM）之上，会自动进行垃圾回收（GC），不需要程序员来管理内存。但是GC正常工作有个前提，就是<strong>开发者不要自己管理内存</strong>。</p>
<p>由于3D游戏会使用大量多媒体资产文件，加载它们会产生大量I/O操作，造成性能下降。为了提高性能，多数游戏引擎都会使用缓存，而使用缓存就意味着 <strong>程序自己管理内存</strong> 。如果缓存使用不当，GC的工作就会受到影响，极有可能造成内存泄露、内存溢出等问题。</p>
<p>本文的目标是：</p>
<ul>
<li>介绍 jME3 中的资产缓存机制；</li>
<li>介绍</li></ul>]]></description><link>https://blog.jmecn.net/asset-cache/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b4e</guid><category><![CDATA[资产管线]]></category><category><![CDATA[jMonkeyEngine]]></category><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Fri, 12 Jan 2018 11:09:29 GMT</pubDate><content:encoded><![CDATA[<p>在 jMonkeyEngine 中，资产缓存（AssetCache）对开发者是个黑盒。绝大多数时间你都不会意识到它的存在，直到有一天程序突然发生内存溢出，抛出一个 <code>OutOfMemoryError</code>。</p>
<p>例如：</p>
<pre><code>Exception in thread &quot;main&quot; java.lang.OutOfMemoryError: Java heap space
</code></pre>
<p>众所周知，Java程序运行于虚拟机（JVM）之上，会自动进行垃圾回收（GC），不需要程序员来管理内存。但是GC正常工作有个前提，就是<strong>开发者不要自己管理内存</strong>。</p>
<p>由于3D游戏会使用大量多媒体资产文件，加载它们会产生大量I/O操作，造成性能下降。为了提高性能，多数游戏引擎都会使用缓存，而使用缓存就意味着 <strong>程序自己管理内存</strong> 。如果缓存使用不当，GC的工作就会受到影响，极有可能造成内存泄露、内存溢出等问题。</p>
<p>本文的目标是：</p>
<ul>
<li>介绍 jME3 中的资产缓存机制；</li>
<li>介绍 AssetManager、AssetCache等常用API；</li>
</ul>
<p>本文的目标<strong>不是：</strong></p>
<ul>
<li>解释类、对象、引用的概念；</li>
<li>解释Java虚拟机的内存布局；</li>
<li>解释Java虚拟机的垃圾回收（GC）原理；</li>
<li>解释什么是强引用、软引用、弱引用、虚引用。</li>
</ul>
<p>Java虚拟机的内存管理是一个很大的话题，本文无法展开深入讨论，重点将聚焦在jME3资产缓存的用法。如果读者对JVM、GC算法等话题感兴趣，我推荐一本书：《深入理解Java虚拟机》。</p>
<h3 id="">缓存基本原理</h3>
<p>不知大家平时是否观察过便利店，店家一般会把香烟摆在门口的柜台里，因为它们最好卖；夏天，便利店通常会把装冰淇淋、冷饮的冰柜摆在门口，也是因为它们最好卖。</p>
<p>把常用的东西放在触手可及的地方，这就是缓存（Cache）的基本思想。</p>
<h4 id="">哈希表</h4>
<p>缓存一般使用哈希表（Hash Table）作为数据结构，采用键值对（Key-Value）的方式进行读写。使用时哈希表时，先要根据 Key 来计算一个 Hash 值，然后再根据 Hash 值映射到内存地址。Hash算法的时间复杂度是 O(1)，速度比I/O要快很多，这是缓存能够提高性能的原因。</p>
<p>Java 中的 HashMap、HashSet、Hashtable 等数据结构都是基于Hash算法实现的，因此常被用于缓存数据。</p>
<h4 id="">缓存命中率</h4>
<p>用户访问缓存时，如果缓存中已经保存了要被访问的数据，称为<strong>命中</strong>；如果缓存中没有要访问的数据，称为<strong>未命中</strong>。 如果缓存未命中，程序就需要去读取文件数据。</p>
<p>缓存<strong>命中率</strong> = 命中次数 / (命中次数 + 未命中次数)，<strong>命中率</strong> 是评价缓存加速效果的重要指标。</p>
<p>为了提高命中率，很容易想到的做法是把数据都塞进缓存中。但如果大部分数据都不会被再次访问，这样做显然会造成内存浪费，实际上也没有无限大的内存供程序使用。如果你真的这么做了，很快就会发生内存溢出。</p>
<pre><code>Exception in thread &quot;main&quot; java.lang.OutOfMemoryError: Java heap space
</code></pre>
<p>正常的做法，要限制缓存的大小，并要评估哪些数据是常用的。尽量把 <strong>热点数据</strong> 放进缓存；把那些不常用的数据从缓存中移除，称为 <strong>缓存淘汰</strong>。</p>
<h4 id="">缓存淘汰算法</h4>
<p><strong>LFU</strong> (Least frequently used, 最不经常使用) 是一种常用的缓存淘汰算法。这种算法的思想很简单：假设公司有10个推销员，到了季度末招聘新人时，客户数最少的那个推销员会被淘汰，让出空位给新人；客户数最多的推销员，肯定是老板眼里的红人（热点）。实现这种算法需要引入一个计数器，统计每个对象被访问的次数。</p>
<p><strong>LRU</strong> (Least recently used，最近最少使用) 与 LFU 非常类似，但这种算法不记录对象的访问次数，而是记录对象被访问的时间。还是那个例子：假设公司有10个销售员，销售员A是个新人，最近一季度获得了10位客户；销售员B是个老人，累计获得了100个客户，但最近一个季度没有获得任何客户。老板虽然很感激销售员B为公司做出的贡献，但还是决定开除他/她。</p>
<p><strong>FIFO</strong> (First in first out, 先进先出) 是另一种常用算法。它的思路也很简单，就是淘汰老人，一般可以使用队列来实现。</p>
<h3 id="jme3">jME3的缓存</h3>
<p>jME3 中有三种不同的资产缓存(AssetCache)，都是基于哈希表的。AssetCache 内部使用 AssetKey 作为查询的“键”，可以缓存任意类型的数据。</p>
<ul>
<li>SimpleAssetCache</li>
<li>WeakRefAssetCache</li>
<li>WeakRefCloneAssetCache</li>
</ul>
<p>出于多线程开发的考虑，jME3的缓存实现必须是线程安全（thread-safe）的，这三种缓存都符合这个要求。如果你要自己实现缓存管理，也必须是线程安全的。</p>
<p>这三种缓存都没有限制内存的大小，也没有实现前面介绍过的缓存淘汰算法。如果你的程序从来都只加载资产，从来不清理缓存，就 <strong>有可能</strong> 发生内存溢出。但这也不是必然的，如果你的程序较小，使用的资产本身并不多，就不会发生这种情况。此外，jME3利用Java的GC机制实现了另一种缓存淘汰算法。</p>
<h4 id="simpleassetcache">SimpleAssetCache</h4>
<p><code>SimpleAssetCache</code> 是 AssetKey 默认的缓存方式。它的内部非常简单，直接使用 <code>ConcurrentHashMap</code> 实现了缓存，以保证线程安全。</p>
<p>由于 <code>ConcurrentHashMap</code> 内部以强引用方式报保存数据，而且没有使用任何缓存淘汰算法，使用这种缓存会导致GC完全失效。如果有大量数据被缓存，不及时清理，就会发生内存溢出。</p>
<p><code>SimpleAssetCache</code> 不管理克隆对象。使用 AssetKey 查询 <code>SimpleAssetCache</code> 命中时，总会直接返回缓存的对象。</p>
<h4 id="weakrefassetcache">WeakRefAssetCache</h4>
<p>jME3 使用 <code>WeakRefAssetCache</code> 来缓存音频数据，但流模式（Stream）的音频数据不缓存。</p>
<p><code>WeakRefAssetCache</code> 和 <code>SimpleAssetCache</code> 一样不管理克隆对象。查询缓存命中时，总会直接返回缓存的对象。</p>
<p><code>WeakRefAssetCache</code> 的内部同样使用 <code>ConcurrentHashMap</code> 实现线程安全缓存，但却没有把要缓存的对象直接存入，而是通过 <code>WeakReference</code> 来保存对象。</p>
<p><code>WeakReference</code> 是 Java 用来描述弱引用关系的类。弱引用的特点是，一旦程序中其他位置不再有强引用指向对象，<code>WeakReference</code> 所引用的对象就会被GC回收。使用 <code>WeakRefAssetCache</code> 意味着缓存数据可能会被 GC 回收，相当于一种另类的缓存淘汰算法。</p>
<h4 id="weakrefcloneassetcache">WeakRefCloneAssetCache</h4>
<p><code>WeakRefCloneAssetCache</code> 与 <code>WeakRefAssetCache</code> 相似，它可以利用GC来回收内存。不同之处在于，查询缓存命中时，可能会<strong>返回对象的克隆</strong>，而 <strong>不是原对象</strong>。该缓存的内部会记录对象被克隆的次数，只有当 <strong>这些克隆对象都被GC回收</strong> 之后，<code>WeakRefCloneAssetCache</code> 中缓存的对象才会被GC回收。</p>
<p>为什么要克隆？为什么不使用 <code>WeakRefAssetCache</code> 或 <code>SimpleAssetCache</code> 呢？</p>
<p>例如游戏中可能会有多个NPC共用同一个模型。使用 <code>WeakRefCloneAssetCache</code> 可以使 NPC 模型文件只读取一次，然后被多次克隆使用。游戏运行时，每个NPC都有自己的动画、空间变换等状态。如果使用同一个对象，就会导致所有NPC的看起来都是完全一样的，并且重叠在空间中的同一个位置。</p>
<p>材质的使用方式也类似：同一种材质会被多个模型，但每个模型的材质参数都是不一样的，需要创建不同的材质实例。</p>
<p><code>WeakRefCloneAssetCache</code> 不能单独使用。缓存对象必须实现 <code>com.jme3.asset.CloneableSmartAsset</code> 接口，否则无法正确克隆对象。此外，一般还要配合 <code>com.jme3.asset.CloneableAssetProcessor</code>使用，这个类负责创建克隆对象。</p>
<h3 id="">使用缓存</h3>
<h4 id="">设置缓存类型</h4>
<p>加载资产时到底会使用何种缓存？</p>
<p>AssetKey 中有一个 <code>getCacheType()</code> 方法，该方法返回了缓存类型。若返回 <code>null</code>，表示不使用缓存。</p>
<p>AssetKey 中的默认实现是这样的：</p>
<pre><code>public Class&lt;? extends AssetCache&gt; getCacheType(){
    return SimpleAssetCache.class;
}
</code></pre>
<p>因此，直接使用 AssetKey 加载资产会使用强引用的方式缓存。</p>
<p>在AudioKey中，该方法是这样实现的：</p>
<pre><code>public Class&lt;? extends AssetCache&gt; getCacheType() {
    if ((stream &amp;&amp; streamCache) || !stream) {
        // Use non-cloning cache
        return WeakRefAssetCache.class;
    } else {
        // Disable caching for streaming audio
        return null;
    }
}
</code></pre>
<p>可以看到，流模式的音频数据是不缓存的。非流模式的数据，会采用弱引用的方式缓存。</p>
<p>在 ModelKey、TextureKey、MaterialKey中，该方法是这样实现的：</p>
<pre><code>public Class&lt;? extends AssetCache&gt; getCacheType(){
    return WeakRefCloneAssetCache.class;
}

public Class&lt;? extends AssetProcessor&gt; getProcessorType(){
    return CloneableAssetProcessor.class;
}
</code></pre>
<p>jME3 中的 <code>Spatial</code>、<code>Texture</code>、<code>Mateiral</code> 均实现了 <code>CloneableSmartAsset</code> 接口。AssetManager在加载这些对象时，会利用 <code>CloneableAssetProcessor</code> 来创建克隆对象，并通知 <code>WeakRefCloneAssetCache</code> 记录克隆的次数。</p>
<h4 id="assetcache">AssetCache接口</h4>
<p>AssetCache 是 jME3 定义的资产缓存接口，其中定义了6个方法。jME3默认的三种缓存都实现了这个接口。</p>
<pre><code>package com.jme3.asset.cache;

import com.jme3.asset.AssetKey;

public interface AssetCache {
    public &lt;T&gt; void addToCache(AssetKey&lt;T&gt; key, T obj);
    public &lt;T&gt; T getFromCache(AssetKey&lt;T&gt; key);
    public boolean deleteFromCache(AssetKey key);
    public void clearCache();
    public &lt;T&gt; void registerAssetClone(AssetKey&lt;T&gt; key, T clone);
    public void notifyNoAssetClone();
}
</code></pre>
<p>AssetCache 的前四个方法，用于添加、查找、删除、清空缓存；后两个方法用于处理缓存的克隆操作。AssetManager 中定义了与前四个方法同名的接口，开发时一般会直接调用AssetManager中提供的方法。</p>
<p>下面的代码演示了如何使用缓存。</p>
<pre><code>package net.jmecn.assets;

import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.system.JmeSystem;

/**
 * 测试缓存
 * @author yanmaoyuan
 *
 */
public class TestCache {

    public static void main(String[] args) {
        // 初始化AssetManager
        AssetManager assetManager = JmeSystem.newAssetManager();
        assetManager.registerLocator(&quot;./&quot;, FileLocator.class);
        assetManager.registerLoader(TextLoader.class, &quot;txt&quot;);

        // 定义AssetKey
        AssetKey&lt;String&gt; key = new AssetKey&lt;String&gt;(&quot;assets/hello.txt&quot;);

        // 加载资源，此时AssetManager会自动缓存数据。
        String str1 = assetManager.loadAsset(key);
        System.out.println(&quot;str1:&quot; + str1);

        // 查找缓存
        String str2 = assetManager.getFromCache(key);
        System.out.println(&quot;str2:&quot; + str2);

        // 删除缓存
        boolean success = assetManager.deleteFromCache(key);
        System.out.println( success ? &quot;删除成功&quot; : &quot;删除失败&quot;);

        // 删除后再次查找缓存
        String str3 = assetManager.getFromCache(key);
        System.out.println(&quot;str3:&quot; + str3);
    }

}
</code></pre>
<p>运行结果：</p>
<pre><code>str1:Hello jMonkeyEngine!

str2:Hello jMonkeyEngine!

删除成功
str3:null
</code></pre>
<p>通过上面的代码，可以说明缓存的基本用法。</p>
<ul>
<li>开发者无需手动调用 <code>addToCache(key, obj)</code> 方法，<code>loadAsset(key)</code> 方法会自动缓存数据。</li>
<li><code>getFromCache(key)</code> 方法用于查找缓存。若命中缓存，就能得到缓存数据；若未命中，则会得到null。</li>
<li><code>deleteFromCache(key)</code> 方法用于删除缓存。若key对应的缓存存在，并且被成功删除，就会返回 true；否则将返回false。</li>
<li>必须定义 <code>AssetKey</code>，否则无法查找、删除缓存数据。</li>
</ul>
<p>代码中没有演示 <code>clearCache()</code> 的用法，它的作用是清空所有缓存。</p>
<p>实际开发时，往往连 <code>getFromCache(key)</code> 方法都用不上，因为 <code>loadAsset(key)</code> 方法会自动查找缓存。</p>
<p>看下面的代码：</p>
<pre><code>public static void main(String[] args) {
    // 初始化AssetManager
    AssetManager assetManager = JmeSystem.newAssetManager();
    assetManager.registerLocator(&quot;./&quot;, FileLocator.class);
    assetManager.registerLoader(TextLoader.class, &quot;txt&quot;);

    // 定义AssetKey
    AssetKey&lt;String&gt; key = new AssetKey&lt;String&gt;(&quot;assets/hello.txt&quot;);

    // 加载资源，此时AssetManager会自动缓存数据。
    String str1 = assetManager.loadAsset(key);
    System.out.println(&quot;str1:&quot; + str1);

    // 再次加载，此时AssetManager将会从缓存中读取数据。
    String str2 = assetManager.loadAsset(key);
    System.out.println(&quot;str2:&quot; + str2);

    // 判断是否为同一个对象
    System.out.println(&quot;str1 == str2 : &quot; + ( str1 == str2 ));
}
</code></pre>
<p>运行结果：</p>
<pre><code>str1:Hello jMonkeyEngine!

str2:Hello jMonkeyEngine!

str1 == str2 : true
</code></pre>
<p>这个结果说明 <code>loadAsset(key)</code> 方法执行时，会先查询缓存。如果缓存命中，就不会发生I/O操作。</p>
<p>需要注意的是，<code>AssetKeky</code> 默认采用了 <code>SimpleAssetCache</code>，导致返回的缓存对象就是原对象，因此最后一行输出为 <code>str1 == str2 : true</code>。</p>
<h4 id="">不使用缓存</h4>
<p>如果你不想使用缓存，可以覆盖 AssetKey 中的 <code>getCacheType()</code> 方法，使其返回 <code>null</code> 。</p>
<pre><code>public static void main(String[] args) {
    // 初始化AssetManager
    AssetManager assetManager = JmeSystem.newAssetManager();
    assetManager.registerLocator(&quot;./&quot;, FileLocator.class);
    assetManager.registerLoader(TextLoader.class, &quot;txt&quot;);

    // 定义AssetKey
    AssetKey&lt;String&gt; key = new AssetKey&lt;String&gt;(&quot;assets/hello.txt&quot;) {
        // 禁用缓存
        public Class&lt;? extends AssetCache&gt; getCacheType(){
            return null;
        }
    };

    // 加载资产
    String str1 = assetManager.loadAsset(key);
    System.out.println(&quot;str1:&quot; + str1);

    // 加载资产
    String str2 = assetManager.loadAsset(key);
    System.out.println(&quot;str2:&quot; + str2);

    // 判断是否为同一个对象
    System.out.println(&quot;str1 == str2 : &quot; + ( str1 == str2 ));
}
</code></pre>
<p>运行结果：</p>
<pre><code>str1:Hello jMonkeyEngine!

str2:Hello jMonkeyEngine!

str1 == str2 : false
</code></pre>
<p>但是这么做的话，就不能再使用 <code>getFromCache(key)</code> 方法了，因为根本就没有缓存。如果强行调用，将会产生异常。</p>
<p>演示代码：</p>
<pre><code>public static void main(String[] args) {
    // 初始化AssetManager
    AssetManager assetManager = JmeSystem.newAssetManager();
    assetManager.registerLocator(&quot;./&quot;, FileLocator.class);
    assetManager.registerLoader(TextLoader.class, &quot;txt&quot;);

    // 定义AssetKey
    AssetKey&lt;String&gt; key = new AssetKey&lt;String&gt;(&quot;assets/hello.txt&quot;) {
        // 禁用缓存
        public Class&lt;? extends AssetCache&gt; getCacheType(){
            return null;
        }
    };

    // 加载资源，此时AssetManager会自动缓存数据。
    String str1 = assetManager.loadAsset(key);
    System.out.println(&quot;str1:&quot; + str1);

    // 查询缓存
    String str2 = assetManager.getFromCache(key);
    System.out.println(&quot;str2:&quot; + str2);

    // 判断是否为同一个对象
    System.out.println(&quot;str1 == str2 : &quot; + ( str1 == str2 ));
}
</code></pre>
<p>运行结果：</p>
<pre><code>str1:Hello jMonkeyEngine!

Exception in thread &quot;main&quot; java.lang.IllegalArgumentException: Key assets/hello.txt specifies no cache.
    at com.jme3.asset.DesktopAssetManager.getFromCache(DesktopAssetManager.java:209)
    at net.jmecn.assets.TestCache.main(TestCache.java:37)
</code></pre>
<h4 id="">弱引用智能拷贝</h4>
<p>如果想要使用 <code>WeakRefCloneAssetCache</code>，加载的对象类型必须实现 <code>CloneableSmartAsset</code> 接口，并且在 AssetKey 中指定缓存类型为 <code>WeakRefCloneAssetCache.class</code>，后处理类型为 <code>CloneableAssetProcessor.class</code>。这样AssetManager就会使用弱引用智能拷贝功能。</p>
<p>代码如下：</p>
<pre><code>package net.jmecn.assets;

import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetManager;
import com.jme3.asset.AssetProcessor;
import com.jme3.asset.CloneableAssetProcessor;
import com.jme3.asset.CloneableSmartAsset;
import com.jme3.asset.cache.AssetCache;
import com.jme3.asset.cache.WeakRefCloneAssetCache;
import com.jme3.system.JmeSystem;

/**
 * 测试缓存
 * @author yanmaoyuan
 *
 */
public class TestCache {
    
    /**
     * 测试用Key
     * @author yanmaoyuan
     *
     */
    private static class DummyKey extends AssetKey&lt;DummyAsset&gt; {
        public DummyKey(String name) {
            super(name);
        }
        @Override
        public Class&lt;? extends AssetCache&gt; getCacheType(){
            // 指定使用弱引用、克隆缓存。
            return WeakRefCloneAssetCache.class;
        }
        @Override
        public Class&lt;? extends AssetProcessor&gt; getProcessorType(){
            // 指定使用智能拷贝。
            return CloneableAssetProcessor.class;
        }
    }

    /**
     * 测试用资产类型
     * @author yanmaoyuan
     *
     */
    private static class DummyAsset implements CloneableSmartAsset {
        private byte[] data;
        public DummyAsset(byte[] data) {
            this.data = data;
        }
        // 实现克隆方法
        @Override public Object clone() {
            return new DummyAsset(data.clone());
        }
        // 实现 CloneableSmartAsset 接口
        private AssetKey key;
        @Override public void setKey(AssetKey key) { this.key = key; }
        @Override public AssetKey getKey() { return key; }
    }
    
    public static void main(String[] args) {
        // 初始化AssetManager
        AssetManager assetManager = JmeSystem.newAssetManager();

        // 创建AssetKey
        DummyKey key = new DummyKey(&quot;dummy&quot;);
        
        // 创建资产数据
        DummyAsset asset = new DummyAsset(new byte[1024]);
        System.out.println(asset);
        
        // 存入缓存
        assetManager.addToCache(key, asset);
        
        // 加载资产
        DummyAsset asset2 = assetManager.loadAsset(key);
        System.out.println(asset2);
        
    }

}
</code></pre>
<p>运行结果：</p>
<pre><code>net.jmecn.assets.TestCache$DummyData@372f7a8d
net.jmecn.assets.TestCache$DummyData@4f023edb
</code></pre>
<p>可以看到，<code>loadAsset(key)</code> 方法返回的并不是刚开始创建的对象，说明AssetManager克隆了原对象。</p>
]]></content:encoded></item><item><title><![CDATA[第十九章：自定义AssetLoader]]></title><description><![CDATA[<p>用Java I/O读取文件，分为三步：</p>
<ul>
<li>打开输入流</li>
<li>读取数据</li>
<li>关闭输入流</li>
</ul>
<p>在jME3中，“读取数据”这一步是由资产加载器（AssetLoader）来实现的。AssetLoader是一个接口，只定义了一个方法：</p>
<pre><code>public interface AssetLoader {
    public Object load(AssetInfo info);
}
</code></pre>
<p>当 AssetManager 在加载游戏资产时，先会调用各种 AssetLocator 的 <code>locate(AssetKey key)</code> 方法去搜索资源，并返回一个 <code>AssetInfo</code> 对象。然后根据文件的<strong>后缀名</strong>来选择一个 <code>AssetLoader</code> 类，调用它的 <code>load(AssetInfo info)</code> 方法来读取和解析数据，最后返回一个具体的对象。</p>
<h3 id="textloader">实现TextLoader</h3>
<p>为了演示 AssetLoader 的具体工作方式，</p>]]></description><link>https://blog.jmecn.net/custom-assetloader/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b4d</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Thu, 11 Jan 2018 15:30:44 GMT</pubDate><content:encoded><![CDATA[<p>用Java I/O读取文件，分为三步：</p>
<ul>
<li>打开输入流</li>
<li>读取数据</li>
<li>关闭输入流</li>
</ul>
<p>在jME3中，“读取数据”这一步是由资产加载器（AssetLoader）来实现的。AssetLoader是一个接口，只定义了一个方法：</p>
<pre><code>public interface AssetLoader {
    public Object load(AssetInfo info);
}
</code></pre>
<p>当 AssetManager 在加载游戏资产时，先会调用各种 AssetLocator 的 <code>locate(AssetKey key)</code> 方法去搜索资源，并返回一个 <code>AssetInfo</code> 对象。然后根据文件的<strong>后缀名</strong>来选择一个 <code>AssetLoader</code> 类，调用它的 <code>load(AssetInfo info)</code> 方法来读取和解析数据，最后返回一个具体的对象。</p>
<h3 id="textloader">实现TextLoader</h3>
<p>为了演示 AssetLoader 的具体工作方式，下面实现一个自定义的 TextLoader 类。</p>
<pre><code>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(&quot;\n&quot;);
        }
        
        // 关闭输入流
        scanner.close();
        in.close();
        
        return sb.toString();
    }

}
</code></pre>
<p>这个类的作用一目了然：逐行读取输入流中的文本数据，最后返回了整个字符串。</p>
<h4 id="">创建测试文件</h4>
<p>为了测试这个TextLoader的作用，在工程目录下创建 <code>assets</code> 文件夹，并在 <code>assets</code> 目录中创建 <code>A.txt</code>、<code>B.xml</code>、<code>C.md</code> 三个文件，它们的内容如下。</p>
<p>assets/A.txt</p>
<pre><code>Hello TextLoader!
This is A.txt
</code></pre>
<p>assets/B.xml:</p>
<pre><code>&lt;root&gt;
    &lt;title&gt;Hello TextLoader&lt;/title&gt;
    &lt;context&gt;This is B.xml&lt;/context&gt;
&lt;/root&gt;
</code></pre>
<p>assets/C.md</p>
<pre><code>Hello `TextLoader`!
This is **C.md**.
</code></pre>
<h4 id="">编写测试类</h4>
<p>只需要有一个AssetInfo对象作为参数，就可以调用 AssetLoader 接口，从而绕过 AssetManager。</p>
<p>事实上，我们连测试文件都不需要，可以直接在内存中定义一个字符串，然后利用ByteArrayInputStream来把字节数据包装成InputStream。</p>
<pre><code>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 = &quot;Hello TextLoader!\nThis is a string in memoery&quot;;
                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();
        }
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>Hello TextLoader!
This is a string in memoery
</code></pre>
<p>不过正常来说，从文件中读取数据是更常用。</p>
<pre><code>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(&quot;assets/A.txt&quot;);
                } 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();
        }
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>Hello TextLoader!
This is A.txt
</code></pre>
<p>前文已经介绍过AssetManager、AssetLocator的作用，使用它们来测试AssetLoader，做法如下。</p>
<pre><code>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(&quot;assets&quot;, FileLocator.class);
        
        // 打开输入流
        AssetInfo info = assetManager.locateAsset(new AssetKey(&quot;B.xml&quot;));

        // 解析文件
        AssetLoader loader = new TextLoader();
        try {
            String result = (String)loader.load(info);
            System.out.println(result);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>&lt;root&gt;
    &lt;title&gt;Hello TextLoader&lt;/title&gt;
    &lt;context&gt;This is B.xml&lt;/context&gt;
&lt;/root&gt;
</code></pre>
<h3 id="">关联后缀名</h3>
<p>调用 <code>AssetManager</code> 中的 <code>registerLoader</code> 方法，可以把某种 AssetLoader 实现类与特定的后缀名关联在一起。在调用 <code>loadAsset</code> 方法时，AssetManager 会根据后缀名去匹配 <code>AssetLoader</code>。</p>
<pre><code>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(&quot;assets&quot;, FileLocator.class);
        assetManager.registerLoader(TextLoader.class, &quot;md&quot;);

        // 加载资产
        String result = (String) assetManager.loadAsset(&quot;C.md&quot;);
        System.out.println(result);
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>Hello `TextLoader`!
This is **C.md**.
</code></pre>
<p>如果希望让一个 AssetLoader 关联多种文件类型，可以这样做：</p>
<pre><code>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(&quot;assets&quot;, FileLocator.class);
        
        // 关联文件后缀名
        assetManager.registerLoader(TextLoader.class, &quot;txt&quot;);
        assetManager.registerLoader(TextLoader.class, &quot;xml&quot;);
        assetManager.registerLoader(TextLoader.class, &quot;md&quot;);

        // 加载资产
        String result = (String) assetManager.loadAsset(&quot;C.md&quot;);
        System.out.println(result);
    }
}
</code></pre>
<p>还有更简单的做法：</p>
<pre><code>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(&quot;assets&quot;, FileLocator.class);
        
        // 关联文件后缀名
        assetManager.registerLoader(TextLoader.class, &quot;txt&quot;, &quot;xml&quot;, &quot;md&quot;);

        // 加载资产
        String result = (String) assetManager.loadAsset(&quot;C.md&quot;);
        System.out.println(result);
    }
}
</code></pre>
<p>这两种做法的结果都是一样的。</p>
<h3 id="">类型转换</h3>
<p>注意，<code>load(AssetInfo info)</code> 方法的返回类型是 <code>Object</code>。由于 Java 中的所有类都是 <code>java.lang.Object</code> 的子类，因此 AssetLoader 才能支持扩展解析任意类型的对象。</p>
<p>问题是 AssetLoader 在返回解析后的对象时，原始的对象类型丢失了，需要进行强制转换才能获得原来的类型。</p>
<pre><code>    // 加载资产
    String result = (String) assetManager.loadAsset(&quot;C.md&quot;);
    System.out.println(result);
</code></pre>
<p>这样进行对象的强制转换，在Java中称为向上转型，是一种不安全的操作。如果类型比匹配，就会产生 <code>ClassCastException</code>。jME3因此设计了“泛型”机制，可以通过AssetKey<t>来指定加载类型。</t></p>
<pre><code>    // 加载资产
    AssetKey&lt;String&gt; key = new AssetKey&lt;String&gt;(&quot;C.md&quot;);
    String result = assetManager.loadAsset(key);
    System.out.println(result);
</code></pre>
<p>使用泛型的前提，是开发者能够确定某种资产返回的数据类型。在JME3中，有几类游戏专用数据类型，已经定义好了对应的泛型AssetKey。</p>
<ul>
<li>3D模型，返回Spatial类型。定义为 <code>public class ModelKey extends AssetKey&lt;Spatial&gt;</code></li>
<li>纹理，返回Texture类型。定义为 <code>public class TextureKey extends AssetKey&lt;Texture&gt;</code></li>
<li>材质，返回Material类型。定义为 <code>public class MaterialKey extends AssetKey&lt;Material&gt;</code></li>
<li>音频文件，返回AudioData。定义为 <code>public class AudioKey extends AssetKey&lt;AudioData&gt;</code></li>
</ul>
<p>如果你需要加载某种特定类型的资产，最好使用泛型AssetKey来限定。</p>
<h3 id="3d">解析自定义3D模型</h3>
<p>为了进一步演示如何使用AssetLoader，下面模仿OBJ格式，定义一种简单的3D模型格式。</p>
<ul>
<li><code>.yan</code> 格式是纯文本文件，每一行表示一种数据结构。</li>
<li><code>#</code> 开头表示是注释，可忽略。</li>
<li><code>v</code> 开头表示三维顶点，后面连续3个浮点数，定义了顶点坐标(x, y, z)。</li>
<li><code>t</code> 开头表示纹理坐标，后面连续2个浮点数，定义了纹理坐标(u, v)。</li>
<li><code>f</code> 开头表示是面，后面连续3个整数，定义了三角形的顶点索引。</li>
</ul>
<p>样例文件如下：model.yan</p>
<pre><code># 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
</code></pre>
<p>在assets目录下创建文本文件 <code>model.yan</code>，并把上述内容写到该文件中。</p>
<h4 id="yanloader">实现YanLoader</h4>
<p>定义 YanLoader 类，用于解析 <code>.yan</code> 格式的文件。</p>
<pre><code>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&lt;Float&gt; vertex = new ArrayList&lt;Float&gt;();
    private List&lt;Float&gt; texCoord = new ArrayList&lt;Float&gt;();
    private List&lt;Integer&gt; face = new ArrayList&lt;Integer&gt;();
    
    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, &quot;Common/MatDefs/Misc/Unshaded.j3md&quot;);
        mat.setColor(&quot;Color&quot;, 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(&quot;#&quot;)) {
                // 跳过注释行
                continue;
            } else if (line.startsWith(&quot;v &quot;)) {
                // 解析顶点
                line = line.substring(2);
                String[] tokens = line.split(&quot;,&quot;);
                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(&quot;t &quot;)) {
                // 解析纹理坐标
                line = line.substring(2);
                String[] tokens = line.split(&quot;,&quot;);
                float a = Float.valueOf(tokens[0].trim());
                float b = Float.valueOf(tokens[1].trim());
                
                // 保存纹理坐标
                texCoord.add(a);
                texCoord.add(b);
            } else if (line.startsWith(&quot;f &quot;)) {
                // 解析面
                line = line.substring(2);
                String[] tokens = line.split(&quot;,&quot;);
                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&lt;count; i++) {
            vb.put(vertex.get(i).floatValue());
        }
        vb.flip();
        
        // 纹理坐标缓存
        count = texCoord.size();
        FloatBuffer uv = BufferUtils.createFloatBuffer(count);
        for(int i=0; i&lt;count; i++) {
            uv.put(texCoord.get(i).floatValue());
        }
        uv.flip();
        
        // 索引缓存
        count = face.size();
        ShortBuffer ib = BufferUtils.createShortBuffer(count);
        for(int i=0; i&lt;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;
    }
}
</code></pre>
<h4 id="">编写测试代码</h4>
<p>编写TestYanLoader类，在jME3环境中加载 <code>model.yan</code> 文件，查看该模型。</p>
<pre><code>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(&quot;assets&quot;, FileLocator.class);
        // 注册解析器
        assetManager.registerLoader(YanLoader.class, &quot;yan&quot;);
        
        // 加载模型
        Spatial model = assetManager.loadAsset(new ModelKey(&quot;model.yan&quot;));
        rootNode.attachChild(model);
    }

    public static void main(String[] args) {
        TestYanLoader app = new TestYanLoader();
        app.start();
    }

}
</code></pre>
<p>运行结果：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/test_yan_loader.png" alt=""></p>
<h3 id="">小结</h3>
<p>AssetLoader的工作机制并不复杂。主要是通过 openStream 获得输入流，剩下的主要是根据文件的数据结构来进行解析。</p>
<p>通过 registerLoader 方法，可以将某种后缀名的文件与特定 AssetLoader 关联起来。如果你不喜欢使用 AssetManager，也可以单独使用 AssetLoader 类，只要提供一个 AssetInfo 实例即可。</p>
<p>使用泛型 AssetKey<t> 可以加载对应某种类型的资源。</t></p>
<p>在 AssetLoader 内部，可以通过 AssetInfo 获得 AssetManager 对象，进而加载文件相关联的其他资源。</p>
]]></content:encoded></item><item><title><![CDATA[第十八章：自定义AssetLocator]]></title><description><![CDATA[<p>前文已经分析了 AssetLocator 的工作原理，本章将介绍如何实现自定义AssetLocator。</p>
<p>jME3 提供了很多不同类型的 AssetLoactor，诸如：</p>
<ul>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/plugins/java/com/jme3/asset/plugins/ClasspathLocator.java">ClasspathLocator</a></li>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/plugins/java/com/jme3/asset/plugins/FileLocator.java">FileLocator</a></li>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/plugins/java/com/jme3/asset/plugins/ZipLocator.java">ZipLocator</a></li>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/plugins/java/com/jme3/asset/plugins/UrlLocator.java">UrlLocator</a></li>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/plugins/java/com/jme3/asset/plugins/HttpZipLocator.java">HttpZipLocator</a></li>
</ul>
<p>如果感兴趣，读者可以直接查阅它们的源代码，阅读源码是学习的最好方式。</p>
<h3 id="">压缩文件格式</h3>
<p>在游戏发布时，开发者经常需要把美术资产打成加密、压缩资源包。每个工作室都可能有自己的加密算法，但通用的压缩算法只有几种：zip, rar, tar, gz, 7z ...</p>
<p>Java语言原生支持 zip 压缩算法，相关类位于 <code>java.util.zip</code> 包中，jME3提供的ZipLocator 即是使用 zip 算法实现的资源查找和解压。</p>
<p>本文将选择 <a href="http://www.7-zip.org/">7-zip</a> 压缩格式作为自定义 AssetLocator 的目标，将开发一个 <code>SevenZAssetLocator</code> 类，用于从 7z 格式的压缩包中提取文件内容。</p>]]></description><link>https://blog.jmecn.net/custom-assetlocator/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b4c</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Thu, 11 Jan 2018 11:07:17 GMT</pubDate><content:encoded><![CDATA[<p>前文已经分析了 AssetLocator 的工作原理，本章将介绍如何实现自定义AssetLocator。</p>
<p>jME3 提供了很多不同类型的 AssetLoactor，诸如：</p>
<ul>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/plugins/java/com/jme3/asset/plugins/ClasspathLocator.java">ClasspathLocator</a></li>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/plugins/java/com/jme3/asset/plugins/FileLocator.java">FileLocator</a></li>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/plugins/java/com/jme3/asset/plugins/ZipLocator.java">ZipLocator</a></li>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/plugins/java/com/jme3/asset/plugins/UrlLocator.java">UrlLocator</a></li>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/plugins/java/com/jme3/asset/plugins/HttpZipLocator.java">HttpZipLocator</a></li>
</ul>
<p>如果感兴趣，读者可以直接查阅它们的源代码，阅读源码是学习的最好方式。</p>
<h3 id="">压缩文件格式</h3>
<p>在游戏发布时，开发者经常需要把美术资产打成加密、压缩资源包。每个工作室都可能有自己的加密算法，但通用的压缩算法只有几种：zip, rar, tar, gz, 7z ...</p>
<p>Java语言原生支持 zip 压缩算法，相关类位于 <code>java.util.zip</code> 包中，jME3提供的ZipLocator 即是使用 zip 算法实现的资源查找和解压。</p>
<p>本文将选择 <a href="http://www.7-zip.org/">7-zip</a> 压缩格式作为自定义 AssetLocator 的目标，将开发一个 <code>SevenZAssetLocator</code> 类，用于从 7z 格式的压缩包中提取文件内容。</p>
<p>当开发完成后，应该能够通过下面的方式来使用它：</p>
<pre><code>public static void main(String[] args) {
    AssetManager assetManager = JmeSystem.newAssetManager();
    assetManager.registerLocator(&quot;assets.7z&quot;, SevenZAssetLocator.class);
    
    AssetInfo info = assetManager.locateAsset(new AssetKey(&quot;assets/hello.txt&quot;));
    System.out.println(info == null? &quot;加载成功&quot;: &quot;加载失败&quot;);
}
</code></pre>
<h3 id="7zipjbind">第一步：下载7ZipJBind</h3>
<p>使用Java程序来解压 7z 文件，需要一些第三方类库。 <a href="http://sevenzipjbind.sourceforge.net/index.html">7-zip Java Binding</a> 是一个开源Java实现，通过调用底层的 C/C++ 代码，实现对 7z 文件的解压。</p>
<p>下载地址： <a href="https://sourceforge.net/projects/sevenzipjbind/files/">https://sourceforge.net/projects/sevenzipjbind/files/</a></p>
<p>因为我用的是Windows系统，下载得到了 <code>sevenzipjbinding-9.20-2.00beta-AllWindows.zip</code>。解压后，在 <code>lib</code> 目录中找到了 <code>sevenzipjbinding.jar</code> 和 <code>sevenzipjbinding-AllWindows.jar</code>，把这两个 jar 文件添加到项目的依赖中即可。</p>
<h3 id="7zipjbind">第二步：初始化7ZipJBind</h3>
<p>根据 SevenZipJBind 官方网站的介绍，先使用下面的代码来初始化SevenZipJBind。</p>
<pre><code>import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipNativeInitializationException;

public class SevenZipJBindingInitCheck {
    public static void main(String[] args) {
        try {
            SevenZip.initSevenZipFromPlatformJAR();
            System.out.println(&quot;7-Zip-JBinding library was initialized&quot;);
        } catch (SevenZipNativeInitializationException e) {
            e.printStackTrace();
        }
    }
}
</code></pre>
<p>运行的结果如下：</p>
<pre><code>7-Zip-JBinding library was initialized
</code></pre>
<p>如果输出不是这个结果，可能你需要检查自己下载的jar文件是否完整。</p>
<h3 id="">第三步：打开压缩文件</h3>
<p>7-zip可以解压很多不同压缩格式的文件，其中就包括 zip 格式。下面尝试用它来打开我刚下载的 <code>sevenzipjbinding-9.20-2.00beta-AllWindows.zip</code> 文件。</p>
<p>SevenZipJBind 官网提供了很多代码，参考样例，编写测试代码如下：</p>
<pre><code>package net.jmecn.assets;

import java.io.IOException;
import java.io.RandomAccessFile;

import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;

public class ListItems {
    
    public static void main(String[] args) {
        String archiveFilename = &quot;sevenzipjbinding-9.20-2.00beta-AllWindows.zip&quot;;

        RandomAccessFile randomAccessFile = null;
        IInArchive inArchive = null;
        try {
            // 打开压缩文件
            randomAccessFile = new RandomAccessFile(archiveFilename, &quot;r&quot;);
            inArchive = SevenZip.openInArchive(null, // 自动检测压缩格式
                    new RandomAccessFileInStream(randomAccessFile));

            // 读取压缩文件中的文件数
            System.out.println(&quot;Count of items in archive: &quot; + inArchive.getNumberOfItems());

            // 遍历目录，查询文件内容
            System.out.println(&quot;   Size   | Compr.Sz. | Filename&quot;);
            System.out.println(&quot;----------+-----------+---------&quot;);
            int itemCount = inArchive.getNumberOfItems();
            for (int i = 0; i &lt; itemCount; i++) {
                System.out.println(String.format(&quot;%9s | %9s | %s&quot;, // 
                        inArchive.getProperty(i, PropID.SIZE),        // 原文件大小
                        inArchive.getProperty(i, PropID.PACKED_SIZE), // 压缩后大小 
                        inArchive.getProperty(i, PropID.PATH)));      // 文件路径
            }
        } catch (Exception e) {
            System.err.println(&quot;Error occurs: &quot; + e);
        } finally {
            // 关闭压缩文件
            if (inArchive != null) {
                try {
                    inArchive.close();
                } catch (SevenZipException e) {
                    System.err.println(&quot;Error closing archive: &quot; + e);
                }
            }
            if (randomAccessFile != null) {
                try {
                    randomAccessFile.close();
                } catch (IOException e) {
                    System.err.println(&quot;Error closing file: &quot; + e);
                }
            }
        }
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>Count of items in archive: 16
    Size   | Compr.Sz. | Filename
-----------+-----------+---------
         0 |         0 | sevenzipjbinding-9.20-2.00beta-AllWindows
       153 |       125 | sevenzipjbinding-9.20-2.00beta-AllWindows\AUTHORS
       141 |       129 | sevenzipjbinding-9.20-2.00beta-AllWindows\CMakeLists.txt
     26444 |      9085 | sevenzipjbinding-9.20-2.00beta-AllWindows\COPYING
      8940 |      3232 | sevenzipjbinding-9.20-2.00beta-AllWindows\ChangeLog
     26444 |      9085 | sevenzipjbinding-9.20-2.00beta-AllWindows\LGPL
      7441 |      2826 | sevenzipjbinding-9.20-2.00beta-AllWindows\README
      1918 |       786 | sevenzipjbinding-9.20-2.00beta-AllWindows\ReleaseNotes.txt
       984 |       590 | sevenzipjbinding-9.20-2.00beta-AllWindows\THANKS
     72665 |     66013 | sevenzipjbinding-9.20-2.00beta-AllWindows\cpp-src.zip
     91407 |     80833 | sevenzipjbinding-9.20-2.00beta-AllWindows\java-src.zip
    278020 |    264548 | sevenzipjbinding-9.20-2.00beta-AllWindows\javadoc.zip
         0 |         0 | sevenzipjbinding-9.20-2.00beta-AllWindows\lib
   2297195 |   2289323 | sevenzipjbinding-9.20-2.00beta-AllWindows\lib\sevenzipjbinding-AllWindows.jar
     71015 |     58970 | sevenzipjbinding-9.20-2.00beta-AllWindows\lib\sevenzipjbinding.jar
    468405 |    451108 | sevenzipjbinding-9.20-2.00beta-AllWindows\website.zip
</code></pre>
<p>根据上面的例子，可以得知下列信息：</p>
<ul>
<li><code>SevenZip.openInArchive()</code> 方法可以打开压缩文件。</li>
<li><code>IInArchive</code> 表示压缩文件，使用完毕应该调用 <code>close()</code> 方法关闭它。</li>
<li><code>IInArchive#getNumberOfItems()</code> 方法可以拿到文件数量。</li>
<li><code>IInArchive</code> 是根据下标来访问每一个文件的。</li>
<li><code>IInArchive#getProperty(i, PropID.PATH)</code> 方法可以访问下标为 <code>i</code> 的文件属性。</li>
<li><code>PropID</code> 是一个枚举类型；<code>PropID.SIZE</code> 表示源文件大小；<code>PropID.PATH</code> 表示文件路径，可用于资源定位。</li>
</ul>
<h3 id="">第四步：解压文件</h3>
<p>上一步得到的信息，足够进行资源定位了，但是还无法拿到具体的文件数据，更别提 InputStream 了。通过查看IInArchive接口的源代码，发现了三个解压接口。</p>
<p><strong>接口一：</strong></p>
<pre><code>public void extract(int[] indices, boolean testMode, IArchiveExtractCallback extractCallback)
        throws SevenZipException;
</code></pre>
<p>第一个参数是一个int[] 数组。根据注释来看，IInArchive支持批量解压缩，只要把被解压的文件下标以数组形式传递给 <code>extract</code> 接口即可。</p>
<p>第二个参数<code>testMode</code>是测试模型。当值为 <code>true</code> 时并不会真的解压数据，只是测试一下文件是否可用；当值为 <code>false</code> 时才会解压数据。</p>
<p>第三个参数 <code>extractCallback</code> 是一个回调接口。extract方法实际上是通过接口回调来获得解压后的数据的。通过实现回调接口，可以自己决定如何处理解压后的数据。</p>
<p>看来免不了要自己实现回调接口了。</p>
<p><strong>接口二：</strong></p>
<pre><code>public ExtractOperationResult extractSlow(int index, ISequentialOutStream outStream) throws SevenZipException;
</code></pre>
<p>这个接口可以解压缩单个文件。第一个参数 index 表示文件的下标（从0开始），第二个参数是一个回调接口，输出的数据通过这个接口来处理。</p>
<p>模式跟前一个接口差不多，只是少了一个参数，并且只解压单个文件。但似乎刚好适合在 AssetLocator 中实现。</p>
<p><strong>接口三：</strong></p>
<pre><code>public ExtractOperationResult extractSlow(int index, ISequentialOutStream outStream, String password)
        throws SevenZipException;
</code></pre>
<p>这个接口与前一个接口几乎一样，只是多了第三个参数 <code>password</code>。如果压缩包中的数据是被加密过的，就可以用 <code>password</code> 参数来解密。当然，前提是你知道解压密码。</p>
<p><strong>IArchiveExtractCallback接口：</strong></p>
<p>这个接口中一共有5个方法要实现。</p>
<p>有两个方法比较简单，它们是用于统计解压进度的。<code>setTotal()</code> 用于记录文件的总字节数， <code>setCompleted</code> 则用于记录当前已解压的字节数。如果不需要统计进度，就不用管这两个方法。</p>
<pre><code>public void setTotal(long total) throws SevenZipException;
public void setCompleted(long complete) throws SevenZipException;
</code></pre>
<p>另外三个方法如下：</p>
<pre><code>public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException;
public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException;
public void setOperationResult(ExtractOperationResult extractOperationResult) throws SevenZipException;
</code></pre>
<p>根据注释来看，SevenZipJBind 的实际解压功能是由 C/C++ 代码实现的。解压算法在执行时，会调用 <code>getStream()</code> 方法来获得一个输出流，用来输出数据。</p>
<p><code>prepareOperation()</code> 方法会在解压之前被调用，用来设置解压请求模式。 ExtractAskMode 是一个枚举类型，它的值包括 EXTRACT、TEST、SKIP、UNKNOWN_ASK_MODE。除了第一个EXTRACT请求外，另外三个都表示并不实际解压。</p>
<p><code>setOperationResult()</code> 会在解压结束后被调用，设置解压缩的结果。ExtractOperationResult 也是一个枚举类型，当它的值为 OK 是表示解压成功，其他值表示解压过程中发生了异常。</p>
<p>看来，不管是 extract 还是 extractSlow 方法，都需要实现 <code>ISequentialOutStream</code> 接口。</p>
<p><strong>ISequentialOutStream接口：</strong></p>
<p>这个接口中只定义了一个方法：</p>
<pre><code>public int write(byte[] data) throws SevenZipException;
</code></pre>
<p>根据该接口的注释来看，这个接口将会被底层的 C/C++ 代码调用。如果需要解压缩的文件比较大，那么这个 <code>write</code> 方法会被调用多次，每次只写入一小段数据。</p>
<h4 id="">测试解压接口</h4>
<p>在大致读完这些接口的源代码和注释之后，已经了解了这些接口的用法。但是还应该写一个测试程序，实际验证一下这些接口的工作机制。</p>
<p>验证思路很简单：用最简单的方式实现这些接口，直接打印方法的参数。然后用接口的实现类调用 <code>extract()</code> 方法，看看运行时会发生什么事情。为了验证write方法是否真的会被调用多次，应该选择解压一个体积较大的文件。</p>
<p>具体代码如下：</p>
<pre><code>package net.jmecn.assets;
import java.io.IOException;
import java.io.RandomAccessFile;

import net.sf.sevenzipjbinding.ExtractAskMode;
import net.sf.sevenzipjbinding.ExtractOperationResult;
import net.sf.sevenzipjbinding.IArchiveExtractCallback;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.ISequentialOutStream;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;

/**
 * 测试解压接口
 * @author yanmaoyuan
 *
 */
public class TestExtract {
    
    public static void main(String[] args) throws IOException {
        TestExtract app = new TestExtract(&quot;sevenzipjbinding-9.20-2.00beta-AllWindows.zip&quot;);
        app.tryExtract(&quot;sevenzipjbinding-9.20-2.00beta-AllWindows\\website.zip&quot;);
    }
    
    private String archiveFilename;

    /**
     * 构造方法，初始化压缩文件路径
     * @param filename
     */
    public TestExtract(String filename) {
        this.archiveFilename = filename;
    }
    
    /**
     * 尝试解压某个文件
     * @param filepath
     * @throws IOException
     */
    public void tryExtract(String filepath) throws IOException{
        // 打开压缩文件
        RandomAccessFile randomAccessFile = new RandomAccessFile(archiveFilename, &quot;r&quot;);
        IInArchive inArchive = SevenZip.openInArchive(null, // 自动选择解压格式
                new RandomAccessFileInStream(randomAccessFile));

        // 查询文件
        int count = inArchive.getNumberOfItems();
        for(int i=0; i&lt;count; i++) {
            String path = (String)inArchive.getProperty(i, PropID.PATH);
            Boolean isFolder = (Boolean)inArchive.getProperty(i, PropID.IS_FOLDER);
            
            if (isFolder) {
                // 文件夹勿需解压，跳过
                continue;
            }
            if (path.equals(filepath)) {
                // 记录开始时间
                long start = System.currentTimeMillis();

                // 解压
                inArchive.extract(new int[]{i}, false, callback);

                // 显示解压用时
                long time = System.currentTimeMillis() - start;
                System.out.println(&quot;time: &quot; + time);
                
                // 终止循环
                break;
            }
        }
        // 关闭压缩文件
        inArchive.close();
    }
    // 实现 ISequentialOutStream 接口，打印方法参数。
    ISequentialOutStream out = new ISequentialOutStream() {
        @Override
        public int write(byte[] data) throws SevenZipException {
            System.out.println(&quot;write:&quot; + data.length);
            return data.length;
        }
    };
    // 实现 IArchiveExtractCallback 接口，打印方法参数。
    IArchiveExtractCallback callback = new IArchiveExtractCallback() {
        @Override
        public void setTotal(long total) throws SevenZipException {
            System.out.println(&quot;total:&quot; + total);
        }
        @Override
        public void setCompleted(long complete) throws SevenZipException {
            System.out.println(&quot;complete:&quot; + complete);
        }
        @Override
        public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException {
            System.out.println(&quot;getStream:&quot; + index + &quot;, &quot; + extractAskMode);
            return out;
        }
        @Override
        public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException {
            System.out.println(&quot;prepare:&quot; + extractAskMode);
        }
        @Override
        public void setOperationResult(ExtractOperationResult extractOperationResult) throws SevenZipException {
            System.out.println(&quot;result:&quot; + extractOperationResult);
        }
    };

}
</code></pre>
<p>运行结果如下：</p>
<pre><code>total:468405
complete:0
getStream:15, EXTRACT
prepare:EXTRACT
write:32768
write:32768
write:32768
write:32768
write:32768
write:32768
write:32768
write:32768
complete:262144
write:32768
write:32768
write:32768
write:32768
write:32768
write:32768
complete:468405
write:9653
result:OK
time: 12
</code></pre>
<p>根据这个运行结果，可以验证很多信息：</p>
<ul>
<li><code>total</code> 和 <code>complete</code> 用于统计解压进度，其中 <code>total</code> 可以拿到文件的原始大小；</li>
<li>另外几个方法的调用顺序是 <code>getStream</code> &gt; <code>prepare</code> &gt; <code>write</code> &gt; <code>write</code> &gt; ... &gt; <code>write</code> &gt; <code>result</code>。</li>
<li><code>complete</code> 和 <code>write</code> 可能处于两个不同的线程中并行执行。</li>
<li><code>extract</code> 方法是同步阻塞的。当数据解压完毕后，才会继续执行后面的代码，显示运行时间。</li>
</ul>
<p>把 getStream() 方法中的代码稍微改一下，直接返回null，看看会发生什么。</p>
<pre><code>    public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException {
        System.out.println(&quot;getStream:&quot; + index + &quot;, &quot; + extractAskMode);
        // return out;
        return null;// &lt;--------------
    }
</code></pre>
<p>运行结果如下：</p>
<pre><code>total:468405
complete:0
getStream:15, EXTRACT
time: 2
</code></pre>
<p>可以看到，在没有提供 <code>ISequentialOutStream</code> 实例的情况下，<code>prepare</code>、<code>write</code>、<code>result</code> 等步骤根本就不会执行。</p>
<h4 id="">解压文件</h4>
<p>既然已经了解 SevenZipJBind 的接口，那么就可以尝试实际解压一个文件了。在 <code>ISequentialOutStream</code> 的实现中，使用 <code>FileOutputStream</code> 来输出数据，把解压后的文件保存到磁盘上。</p>
<p>代码如下：</p>
<pre><code>package net.jmecn.assets;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

import net.sf.sevenzipjbinding.ExtractAskMode;
import net.sf.sevenzipjbinding.ExtractOperationResult;
import net.sf.sevenzipjbinding.IArchiveExtractCallback;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.ISequentialOutStream;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;

/**
 * 测试解压接口
 * @author yanmaoyuan
 *
 */
public class TestExtract {
    
    public static void main(String[] args) throws IOException {
        TestExtract app = new TestExtract(&quot;sevenzipjbinding-9.20-2.00beta-AllWindows.zip&quot;);
        app.tryExtract(&quot;sevenzipjbinding-9.20-2.00beta-AllWindows\\website.zip&quot;);
    }
    
    private String archiveFilename;
    // 文件输出流
    private FileOutputStream output;// &lt;------------------

    /**
     * 构造方法，初始化压缩文件路径
     * @param filename
     */
    public TestExtract(String filename) {
        this.archiveFilename = filename;
    }
    
    /**
     * 尝试解压某个文件
     * @param filepath
     * @throws IOException
     */
    public void tryExtract(String filepath) throws IOException{
        // 打开压缩文件
        RandomAccessFile randomAccessFile = new RandomAccessFile(archiveFilename, &quot;r&quot;);
        IInArchive inArchive = SevenZip.openInArchive(null, // 自动选择解压格式
                new RandomAccessFileInStream(randomAccessFile));

        // 查询文件
        int count = inArchive.getNumberOfItems();
        for(int i=0; i&lt;count; i++) {
            String path = (String)inArchive.getProperty(i, PropID.PATH);
            Boolean isFolder = (Boolean)inArchive.getProperty(i, PropID.IS_FOLDER);
            
            if (isFolder) {
                // 文件夹勿需解压，跳过
                continue;
            }
            if (path.equals(filepath)) {
                
                // 记录开始时间
                long start = System.currentTimeMillis();

                // 检查文件夹是否存在，不存在则创建
                int n = filepath.lastIndexOf(&quot;\\&quot;);
                String folder = filepath.substring(0, n);
                File dir = new File(folder);
                if(!dir.exists())
                    dir.mkdirs();

                // 打开文件流
                File file = new File(filepath);
                output = new FileOutputStream(file);//&lt;------------
                
                // 解压
                inArchive.extract(new int[]{i}, false, callback);
                
                // 关闭文件流
                output.flush();
                output.close();
                output = null;
                
                // 显示解压用时
                long time = System.currentTimeMillis() - start;
                System.out.println(&quot;time: &quot; + time);
                
                // 终止循环
                break;
            }
        }
        
        
        // 关闭压缩文件
        inArchive.close();
    }
    // 实现 ISequentialOutStream 接口，打印方法参数。
    ISequentialOutStream out = new ISequentialOutStream() {
        @Override
        public int write(byte[] data) throws SevenZipException {
            try {
                if (output != null) {
                    // 写文件
                    output.write(data);//&lt;----------
                }
            } catch (IOException e) {
                throw new SevenZipException(e);
            }
            
            return data.length;
        }
    };
    
    // 实现 IArchiveExtractCallback 接口，打印方法参数。
    IArchiveExtractCallback callback = new IArchiveExtractCallback() {
        @Override
        public void setTotal(long total) throws SevenZipException {
            System.out.println(&quot;total:&quot; + total);
        }
        @Override
        public void setCompleted(long complete) throws SevenZipException {
            System.out.println(&quot;complete:&quot; + complete);
        }
        @Override
        public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException {
            System.out.println(&quot;getStream:&quot; + index + &quot;, &quot; + extractAskMode);
            return out;
        }
        @Override
        public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException {
            System.out.println(&quot;prepare:&quot; + extractAskMode);
        }
        @Override
        public void setOperationResult(ExtractOperationResult extractOperationResult) throws SevenZipException {
            System.out.println(&quot;result:&quot; + extractOperationResult);
        }
    };

}
</code></pre>
<p>运行结果。</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/extract_result.png" alt=""></p>
<h3 id="assetlocator">第五步：自定义AssetLocator</h3>
<p>通过前四步，对SevenZipJBind如何查询、如何解压文件已经有所了解。下面定义一个 SevenZAssetLocator，使用SevenZipJBind 来定位压缩包中的文件。</p>
<h4 id="assetlocator">实现AssetLocator</h4>
<p>首先定义SevenZAssetLocator，实现 AssetLocator 接口。</p>
<pre><code>package net.jmecn.assets;

import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLocator;
import com.jme3.asset.AssetManager;

/**
 * 7z 资产定位器
 * @author yanmaoyuan
 *
 */
public class SevenZAssetLocator implements AssetLocator {
    
    @Override
    public void setRootPath(String rootPath) {
    }

    @Override
    public AssetInfo locate(AssetManager manager, AssetKey key) {
        return null;
    }
}
</code></pre>
<p>AssetLocator 接口中有两个方法：</p>
<ul>
<li><code>public void setRootPath(String rootPath)</code> 用于定义查找资产的根目录。对于 SevenZAssetLocator 来说，这个方法将传入压缩文件所在的路径。</li>
<li><code>public AssetInfo locate(AssetManager manager, AssetKey key)</code> 用于查找资源，返回AssetInfo 对象。</li>
</ul>
<p>根据前文的介绍，可以很容易地实现这两个方法。</p>
<pre><code>package net.jmecn.assets;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.HashMap;

import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetLocator;
import com.jme3.asset.AssetManager;

import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;

/**
 * 7z 资产定位器
 * @author yanmaoyuan
 *
 */
public class SevenZAssetLocator implements AssetLocator {

    // 压缩文件
    private IInArchive inArchive;
    // 文件路径缓存，用来加速查询时间。
    private HashMap&lt;String, Integer&gt; cache;
    
    @Override
    public void setRootPath(String rootPath) {
        try {
            // 以只读模式打开压缩包
            RandomAccessFile randomAccessFile = new RandomAccessFile(rootPath, &quot;r&quot;);
            inArchive = SevenZip.openInArchive(null,// 自动选择解压格式
                    new RandomAccessFileInStream(randomAccessFile));
            
            // 统计文件数量，并缓存文件路径
            cache = new HashMap&lt;String, Integer&gt;();
            int count = inArchive.getNumberOfItems();
            for(int i=0; i&lt;count; i++) {
                Boolean isFolder = (Boolean)inArchive.getProperty(i, PropID.IS_FOLDER);
                if (isFolder) {
                    continue;// 文件夹勿需解压，跳过
                }
                
                String path = (String)inArchive.getProperty(i, PropID.PATH);
                // 统一文件分隔符
                path = path.replaceAll(&quot;\\\\&quot;, &quot;/&quot;);
                // 缓存文件路径
                cache.put(path, i);
            }
        } catch (IOException e) {
            throw new AssetLoadException(&quot;Failed to open archive file: &quot; + rootPath, e);
        }
    }

    @Override
    public AssetInfo locate(AssetManager manager, AssetKey key) {
        // 获得文件路径
        String name = key.getName();

        // 统一文件分隔符
        name = name.replace(&quot;\\\\&quot;, &quot;/&quot;);
        
        if(name.startsWith(&quot;/&quot;))
            name = name.substring(1);
        
        
        // 查找文件索引
        Integer index = cache.get(name);
        System.out.println(index);
        return null;
    }
}
</code></pre>
<p>在上面的代码中，<code>setRootPath</code> 方法的主要作用是打开压缩包，并使用HashMap来缓存压缩包中的文件路径。这样在 <code>locate</code> 方法中就可以直接查询文件的索引。</p>
<h4 id="assetinfo">实现AssetInfo</h4>
<p>目前 <code>locate</code> 方法直接返回了 null。想要正常使用，必须返回一个AssetInfo对象才行。</p>
<p>用户不需要关心这个 AssetInfo 对象的具体实现，因此我直接在 SevenZAssetLocator 类中定义了一个私有内部类 SevenZipAssetInfo，并让它实现 AssetInfo 中的 <code>openStream()</code> 方法。</p>
<p>SevenZipAssetInfo 类还需要负责解压文件数据。当用户调用 <code>openStream()</code> 方法时，需要返回一个 InputStream 对象，把解压后的数据交给客户。为了实现文件解压，这个类还需要实现 <code>ISequentialOutStream</code>、<code>IArchiveExtractCallback</code> 这两个接口。</p>
<pre><code>    @Override
    public AssetInfo locate(AssetManager manager, AssetKey key) {
        // 获得文件路径
        String name = key.getName();

        // 统一文件分隔符
        name = name.replace(&quot;\\\\&quot;, &quot;/&quot;);
        
        if(name.startsWith(&quot;/&quot;))
            name = name.substring(1);
        
        
        // 查找文件索引
        Integer index = cache.get(name);
        if (index != null) {
            return new SevenZipAssetInfo(manager, key, index);
        } else {
            return null;
        }
    }

    private class SevenZipAssetInfo extends AssetInfo implements ISequentialOutStream, IArchiveExtractCallback {
        private int index = -1;// 待解压的文件索引
        
        public SevenZipAssetInfo(AssetManager manager, AssetKey key, int index) {
            super(manager, key);
            this.index = index;
        }

        @Override
        public InputStream openStream() {
            return null;
        }

        @Override
        public int write(byte[] data) throws SevenZipException {
            return 0;
        }

        @Override
        public void setTotal(long total) throws SevenZipException {}

        @Override
        public void setCompleted(long complete) throws SevenZipException {}

        @Override
        public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException {
            return null;
        }
        @Override
        public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException {}
        @Override
        public void setOperationResult(ExtractOperationResult result) throws SevenZipException {}
    }
</code></pre>
<p>前文已经实现了文件解压，做法是通过 <code>FileOutputStream</code> 把数据写到磁盘文件中。但现在需要的是一个 InputStream 对象，又应该怎么做呢？</p>
<p>一种比较简单的办法，是创建一个字节数组 <code>byte[] data</code>，把解压后的数据保存到内存中。然后再用 <code>ByteArrayInputStream</code> 来包装这个数组，提供给客户使用。按照同样的思路，还可以使用 Java nio 中的 ByteBuffer 来做到同样的事。</p>
<p>这么做的风险在于，如果被解压的文件较大，就可能发生内存溢出的错误。因为文件数据是直接缓存在Java虚拟机的堆内存中的。不过考虑到单个游戏资产文件一般不会太大，应该还是可以接受的。</p>
<p>完整的实现代码如下：</p>
<pre><code>package net.jmecn.assets;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.HashMap;

import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetLoadException;
import com.jme3.asset.AssetLocator;
import com.jme3.asset.AssetManager;

import net.sf.sevenzipjbinding.ExtractAskMode;
import net.sf.sevenzipjbinding.ExtractOperationResult;
import net.sf.sevenzipjbinding.IArchiveExtractCallback;
import net.sf.sevenzipjbinding.IInArchive;
import net.sf.sevenzipjbinding.ISequentialOutStream;
import net.sf.sevenzipjbinding.PropID;
import net.sf.sevenzipjbinding.SevenZip;
import net.sf.sevenzipjbinding.SevenZipException;
import net.sf.sevenzipjbinding.impl.RandomAccessFileInStream;

/**
 * 7z 资产定位器
 * @author yanmaoyuan
 *
 */
public class SevenZAssetLocator implements AssetLocator {

    // 压缩文件
    private IInArchive inArchive;
    // 文件路径缓存，用来加速查询时间。
    private HashMap&lt;String, Integer&gt; cache;
    
    @Override
    public void setRootPath(String rootPath) {
        try {
            // 以只读模式打开压缩包
            RandomAccessFile randomAccessFile = new RandomAccessFile(rootPath, &quot;r&quot;);
            inArchive = SevenZip.openInArchive(null,// 自动选择解压格式
                    new RandomAccessFileInStream(randomAccessFile));
            
            // 统计文件数量，并缓存文件路径
            cache = new HashMap&lt;String, Integer&gt;();
            int count = inArchive.getNumberOfItems();
            for(int i=0; i&lt;count; i++) {
                Boolean isFolder = (Boolean)inArchive.getProperty(i, PropID.IS_FOLDER);
                if (isFolder) {
                    continue;// 文件夹勿需解压，跳过
                }
                
                String path = (String)inArchive.getProperty(i, PropID.PATH);
                // 统一文件分隔符
                path = path.replaceAll(&quot;\\\\&quot;, &quot;/&quot;);
                // 缓存文件路径
                cache.put(path, i);
            }
        } catch (IOException e) {
            throw new AssetLoadException(&quot;Failed to open archive file: &quot; + rootPath, e);
        }
    }

    @Override
    public AssetInfo locate(AssetManager manager, AssetKey key) {
        // 获得文件路径
        String name = key.getName();

        // 统一文件分隔符
        name = name.replace(&quot;\\\\&quot;, &quot;/&quot;);
        
        if(name.startsWith(&quot;/&quot;))
            name = name.substring(1);
        
        
        // 查找文件索引
        Integer index = cache.get(name);
        if (index != null) {
            return new SevenZipAssetInfo(manager, key, index);
        } else {
            return null;
        }
    }

    private class SevenZipAssetInfo extends AssetInfo implements ISequentialOutStream, IArchiveExtractCallback {
        private ByteArrayOutputStream out;
        private ExtractOperationResult result;
        
        private int index = -1;// 待解压的文件索引
        
        public SevenZipAssetInfo(AssetManager manager, AssetKey key, int index) {
            super(manager, key);
            this.index = index;
        }

        @Override
        public InputStream openStream() {
            try {
                result = null;
                
                // 解压数据
                inArchive.extract(new int[]{index}, false, this);
                
                // 解压失败
                if (result != ExtractOperationResult.OK) {
                    throw new AssetLoadException(&quot;Extracting operation error: &quot; + result);
                }
                
                // 解压成功，获得解压后的数据。
                byte[] data = out.toByteArray();
                out = null;
                
                // 返回一个内存字节输入流
                return new ByteArrayInputStream(data);
            } catch (IOException e) {
                throw new AssetLoadException(&quot;Failed to extract file: &quot; + index, e);
            }
        }

        @Override
        public int write(byte[] data) throws SevenZipException {
            try {
                // 把数据写到内存字节流中。
                out.write(data);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return data.length;
        }

        @Override
        public void setTotal(long total) throws SevenZipException {
            System.out.println(total);
            // 根据数据总长度，初始化内存字节输出流。
            // FIXME 若内存不足，可能发生内存溢出。
            out = new ByteArrayOutputStream((int)total);
        }

        @Override
        public void setCompleted(long complete) throws SevenZipException {}

        @Override
        public ISequentialOutStream getStream(int index, ExtractAskMode extractAskMode) throws SevenZipException {
            // 非解压模式下，不解压数据。
            if (extractAskMode != ExtractAskMode.EXTRACT)
                return null;
            
            return this;
        }
        @Override
        public void prepareOperation(ExtractAskMode extractAskMode) throws SevenZipException {}
        @Override
        public void setOperationResult(ExtractOperationResult result) throws SevenZipException {
            // 记录解压结果
            this.result = result;
        }
    }
}
</code></pre>
<h4 id="">测试程序</h4>
<p>编写一个测试程序：</p>
<pre><code>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.AssetKey;
import com.jme3.asset.AssetManager;
import com.jme3.system.JmeSystem;

/**
 * 测试SevenZAssetLocator
 * @author yanmaoyuan
 *
 */
public class TestSevenZ {
    
    public static void main(String[] args) {
        AssetManager assetManager = JmeSystem.newAssetManager();
        assetManager.registerLocator(&quot;sevenzipjbinding-9.20-2.00beta-AllWindows.zip&quot;, SevenZAssetLocator.class);
        
        AssetInfo info = assetManager.locateAsset(new AssetKey(&quot;sevenzipjbinding-9.20-2.00beta-AllWindows/AUTHORS&quot;));
        System.out.println(info != null? &quot;加载成功&quot;: &quot;加载失败&quot;);
        
        if (info != null) {
            // 读取文件内容
            InputStream in = info.openStream();
            String line = null;

            // 逐行读取，并打印到控制台。
            Scanner scanner = new Scanner(in);
            while(scanner.hasNextLine()) {
                line = scanner.nextLine();
                System.out.println(line);
            }

            // 关闭输入流
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
</code></pre>
<p>运行结果：</p>
<pre><code>加载成功
153
Authors of SevenZipJBinding.
See also the files THANKS, ChangeLog and git history.

Boris Brodski has initially designed and implemented 7-Zip-JBinding.</code></pre>
]]></content:encoded></item><item><title><![CDATA[第十七章：AssetLocator原理]]></title><description><![CDATA[<p>先总结一下前面几章中讲过的概念：</p>
<p>用Java I/O读取文件，分为三步：</p>
<ul>
<li>打开输入流</li>
<li>读取数据</li>
<li>关闭输入流</li>
</ul>
<p>在jME3中，“打开输入流”这一步是由资产定位器（AssetLocator）来实现的。</p>
<p>通过调用 AssetManager 中的 <code>registerLocator(String rootPath, Class locatorClass)</code> 方法，可以配置一对 <strong>资产根目录</strong> 和 <strong>资产定位器</strong> 的关系。接下来再使用 AssetManager 来加载资产，就会自动去这些目录下搜索文件。</p>
<p>本文会进一步分析AssetLocator的工作原理，以及 AssetManager 中的相关接口。</p>
<h3 id="assetinfo">AssetInfo</h3>
<p>在分析AssetLocator之前，请容许我再啰嗦一次，先分析 AssetInfo 类。</p>
<p>在传统的Java I/O操作中，打开输入流这一步的结果一般会得到InputStream对象。由于在解析数据时会需要一些额外的信息，jME3 把 InputStream 包装到了</p>]]></description><link>https://blog.jmecn.net/principle-of-asset-locator/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b4b</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Thu, 11 Jan 2018 06:27:11 GMT</pubDate><content:encoded><![CDATA[<p>先总结一下前面几章中讲过的概念：</p>
<p>用Java I/O读取文件，分为三步：</p>
<ul>
<li>打开输入流</li>
<li>读取数据</li>
<li>关闭输入流</li>
</ul>
<p>在jME3中，“打开输入流”这一步是由资产定位器（AssetLocator）来实现的。</p>
<p>通过调用 AssetManager 中的 <code>registerLocator(String rootPath, Class locatorClass)</code> 方法，可以配置一对 <strong>资产根目录</strong> 和 <strong>资产定位器</strong> 的关系。接下来再使用 AssetManager 来加载资产，就会自动去这些目录下搜索文件。</p>
<p>本文会进一步分析AssetLocator的工作原理，以及 AssetManager 中的相关接口。</p>
<h3 id="assetinfo">AssetInfo</h3>
<p>在分析AssetLocator之前，请容许我再啰嗦一次，先分析 AssetInfo 类。</p>
<p>在传统的Java I/O操作中，打开输入流这一步的结果一般会得到InputStream对象。由于在解析数据时会需要一些额外的信息，jME3 把 InputStream 包装到了 AssetInfo 类中，并利用它来传递其他数据，诸如AssetKey和AssetManager对象。</p>
<p>即是说，AssetLocator 在定位到某个资产文件后，并不是直接返回一个InputStream对象，而是返回一个AssetInfo对象。通过AssetInfo对象，就可以拿到InputStream。</p>
<p>需要注意的是， jME3 中原始的 AssetInfo 是一个抽象类，是不能直接实例化的。<code>openStream()</code> 方法是一个抽象方法，需要由不同的子类来实现。</p>
<pre><code>public abstract InputStream openStream();
</code></pre>
<p>这样做很好理解。因为对于不同来源的数据，Java I/O 中有不同的InputStream实现类。例如：</p>
<ul>
<li>文件输入流 new FileInputStream(&quot;/var/assets/example.png&quot;);</li>
<li>Socket输入流 socket.getInputStream();</li>
<li>Zip解压输入流 new ZipInputStream();</li>
<li>Class加载输入流 class.getResourceAsStream(&quot;/net/jmecn/assets/Main.class&quot;);</li>
</ul>
<p>基本上每个 AssetLocator 都会定义不同的 AssetInfo 实现类。例如 FileLocator，它使用私有内部类 AssetInfoFile 来打开FileInputStream ，供 AssetLoader 使用。</p>
<pre><code>private static class AssetInfoFile extends AssetInfo {

    private File file;

    public AssetInfoFile(AssetManager manager, AssetKey key, File file){
        super(manager, key);
        this.file = file;
    }

    @Override
    public InputStream openStream() {
        try{
            return new FileInputStream(file);
        }catch (FileNotFoundException ex){
            // NOTE: Can still happen even if file.exists() is true, e.g.
            // permissions issue and similar
            throw new AssetLoadException(&quot;Failed to open file: &quot; + file, ex);
        }
    }
}
</code></pre>
<p>实际使用时不需要关心 AssetInfo 的实现，只需要调用 <code>openStream()</code> 获得 <code>InputStream</code> 即可。</p>
<h3 id="">示例</h3>
<p>下面我将创建一个JME3工程，并编写测试类 <code>net.jmecn.assets.TestAssetLocator</code> 来演示它的作用。</p>
<pre><code>package net.jmecn.assets;
import java.io.IOException;
import java.io.InputStream;

import com.jme3.asset.AssetInfo;
import com.jme3.asset.AssetKey;
import com.jme3.asset.AssetManager;
import com.jme3.asset.plugins.ClasspathLocator;
import com.jme3.system.JmeSystem;

/**
 * 演示 AssetInfo 的作用
 * @author yanmaoyuan
 *
 */
public class TestAssetLocator {

    public final static String URL = &quot;net/jmecn/assets/TestAssetLocator.class&quot;;
    public static void main(String[] args) {
        // 创建AssetManager
        AssetManager assetManager = JmeSystem.newAssetManager();
        
        // 注册ClasspathLocator
        assetManager.registerLocator(&quot;/&quot;, ClasspathLocator.class);
        
        // 定位资产
        AssetInfo info = assetManager.locateAsset(new AssetKey(URL));
        
        if (info == null) {
            // 没有找到资产
            System.out.println(&quot;Asset not found!&quot;);
        } else {
            AssetKey key = info.getKey();
            AssetManager manager = info.getManager();
            
            System.out.println(&quot;info:&quot; + info);
            System.out.println(&quot;key:&quot; + key);
            System.out.println(&quot;manager:&quot; + manager);
            
            InputStream in = info.openStream();
            try {
                // TODO 读取数据
                int len = in.available();
                System.out.println(&quot;len:&quot; + len);
                // 关闭输入流
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
</code></pre>
<p>上面的代码通过 AssetManager 的 <code>locateAsset()</code> 方法查找一个 .class 文件，查询结果为一个 AssetInfo 对象。通过 AssetInfo 的 <code>openStream()</code> 方法获得了InputStream，显示了 .class 文件的字节数。</p>
<p>运行结果：</p>
<pre><code>info:com.jme3.asset.plugins.UrlAssetInfo[key=net/jmecn/assets/TestAssetLocator.class]
key:net/jmecn/assets/TestAssetLocator.class
manager:com.jme3.asset.DesktopAssetManager@2ff4acd0
len:2135
</code></pre>
<h4 id="assetmanager">创建AssetManager</h4>
<p>在上面的代码中，我并没有让 TestAssetLocator 继承 SimpleApplication 类，因为用不着3D渲染。但是不继承 SimpleApplication，就不能直接使用 JME3 系统创建的 AssetManager。所以我通过这行代码自己创建了一个：</p>
<pre><code>        // 创建AssetManager
        AssetManager assetManager = JmeSystem.newAssetManager();
</code></pre>
<p>当然，也可以这样做：</p>
<pre><code>        // 创建PC专用AssetManager
        AssetManager assetManager = new DesktopAssetManager();
</code></pre>
<p>或者这样做：</p>
<pre><code>        // 创建Android专用AssetManager
        AssetManager assetManager = new AndroidAssetManager();
</code></pre>
<p>后两种方法也完全可以做到同样的事，只是 <code>JmeSystem</code> 使用“代理模式”来创建 <code>AssetManager</code>，可以提供更好的平台兼容性。</p>
<h4 id="classpathlocator">ClasspathLocator</h4>
<p>示例代码中定义了如下的资源路径：</p>
<pre><code>    public final static String URL = &quot;net/jmecn/assets/TestAssetLocator.class&quot;;
</code></pre>
<p>这个class文件是由JDK编译生成的，源文件就是测试类 <code>net.jmecn.assets.TestAssetLocator.java</code>。在不同的开发环境中，该class文件生成的位置是不一样的：</p>
<ul>
<li>Eclipse中的Java工程，默认会把class文件编译到工程的 <code>bin</code> 目录下；</li>
<li>IDEA 中的Java工程，默认会把class文件输出到 <code>out/production</code> 目录下；</li>
<li>Maven 和 Gradle项目一般会把class文件生成到 <code>build/target</code> 目录下；</li>
<li>Java EE项目，通常会把class文件生成到 <code>WebContent/WEB-INF/classes</code> 目录下；</li>
</ul>
<p>Java虚拟机在工作时，会去这些路径下加载已编译的class文件，这种路径叫做 <code>classpath</code>。<code>classpath</code> 不止一个。在开发Java项目时，通常不会只使用自己编写的类。项目所依赖的各种 jar 文件中保存了其他人开发的 class，这些 jar 文件也会被添加到 <code>classpath</code> 中。这些 <code>classpath</code> 都是由Java虚拟机管理的。</p>
<p><code>ClasspathLocator</code> 的主要作用就是在 <code>classpath</code> 中查找所需的文件。不管使用什么IDE，不管文件是被打包成jar、还是直接放在目录中，都可以被 <code>ClasspathLocator</code> 识别，因为它是依靠Java虚拟机的类加载机制工作的。</p>
<p>下面这行代码的作用是告知 AssetManager，可以在 <code>classpath</code> 的根目录下查找资源。</p>
<pre><code>        // 注册ClasspathLocator
        assetManager.registerLocator(&quot;/&quot;, ClasspathLocator.class);
</code></pre>
<p>如果我想只查找 <code>net.jmecn</code> 包下的资源，就应该这么做：</p>
<pre><code>        // 注册ClasspathLocator
        assetManager.registerLocator(&quot;/net/jmecn&quot;, ClasspathLocator.class);
</code></pre>
<p>相对的，资源的URL也得缩短：</p>
<pre><code>    public final static String URL = &quot;assets/TestAssetLocator.class&quot;;
</code></pre>
<h4 id="locateasset">locateAsset</h4>
<p>有了 AssetManager，也注册好了 AssetLocator，就可以使用 AssetManager 来定位资产了。</p>
<pre><code>        // 定位资产
        AssetInfo info = assetManager.locateAsset(new AssetKey(URL));
</code></pre>
<p>AssetInfo 保存了 AssetManager 搜索资源的结果。如果 <code>info == null</code>，就说明没有找到任何东西，正常流程下会产生 <code>AssetNotFoundException</code>；如果 <code>info != null</code>，就可以通过它打开 InputSteam 了。</p>
<pre><code>        if (info == null) {
            // 没有找到资产
            System.out.println(&quot;Asset not found!&quot;);
        } else {
            AssetKey key = info.getKey();
            AssetManager manager = info.getManager();
            
            System.out.println(&quot;info:&quot; + info);
            System.out.println(&quot;key:&quot; + key);
            System.out.println(&quot;manager:&quot; + manager);
            
            InputStream in = info.openStream();
            try {
                // TODO 读取数据
                int len = in.available();
                System.out.println(&quot;len:&quot; + len);
                // 关闭输入流
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
</code></pre>
<p>在jME3默认的资产加载流程中，开发者是不需要自己去调用 <code>locateAsset()</code> 的，最常使用的是 <code>loadAsset()</code>、<code>loadModel()</code>、<code>loadTexture()</code> 等方法。但如果你需要自己管理资产加载流程，或者根据资产是否存在来做一些处理， <code>locateAsset()</code> 方法的作用就很大了。</p>
<p>另外，<code>AssetManager</code> 需要通过 <code>AssetLoader</code> 才能解析 <code>InputStream</code> 中的数据。如果没有对应的AssetLoader，可以通过 <code>locateAsset()</code> 方法拿到 <code>InputStream</code> 后自己解析数据。</p>
<h3 id="jme3">在jME3中读取文件</h3>
<p>问：如何在jME3程序中读文件？</p>
<p>创建一个JME3工程，并在 assets 目录中创建一个文本文件 <code>hello.txt</code>，内容就一行文字：</p>
<pre><code>Hello jMonkeyEngine!
</code></pre>
<p>下面通过多个例子，演示如何读取这个文件。</p>
<h4 id="javaio">使用JavaIO</h4>
<p>很多Java程序员在接触 “框架” 后会养成一种思维定式，认为什么事都应该按照 “框架” 的方式来做。完全忘记了 “框架” 也是用Java开发的，既然是Java的类和对象，自然就可以用Java I/O来读写文件。</p>
<p>事实上，你完全可以不使用 <code>AssetManager</code> 来读文件。</p>
<p>编写一个测试类，读取这个文件：</p>
<pre><code>package net.jmecn.assets;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

public class TestReadFile {

    public static void main(String[] args) {
        try {
            // 打开文件
            File file = new File(&quot;assets/hello.txt&quot;);
            InputStream in = new FileInputStream(file);
    
            // 读取文件
            Scanner scanner = new Scanner(in);
            String line = scanner.nextLine();
            System.out.println(line);
            
            // 关闭文件
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}
</code></pre>
<p>输入结果：</p>
<pre><code>Hello jMonkeyEngine!
</code></pre>
<p>那么，如果让 TestReadFile 类继承 SimpleApplication 类，代码又应该怎么写呢？</p>
<p>这么写：</p>
<pre><code>package net.jmecn.assets;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;

import com.jme3.app.SimpleApplication;

public class TestReadFile extends SimpleApplication {

    @Override
    public void simpleInitApp() {
        try {
            // 打开文件
            File file = new File(&quot;assets/hello.txt&quot;);
            InputStream in = new FileInputStream(file);
    
            // 读取文件
            Scanner scanner = new Scanner(in);
            String line = scanner.nextLine();
            System.out.println(line);
            
            // 关闭文件
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        TestReadFile app = new TestReadFile();
        app.start();
    }

}
</code></pre>
<p>运行结果：</p>
<pre><code>一月 11, 2018 12:32:06 下午 com.jme3.system.JmeDesktopSystem initialize
信息: Running on jMonkeyEngine 3.2-stable
 * Branch: HEAD
 * Git Hash: 95d33e6
 * Build Date: 2018-01-05
一月 11, 2018 12:32:11 下午 com.jme3.system.lwjgl.LwjglContext printContextInitInfo
信息: LWJGL 2.9.3 context running on thread jME3 Main
 * Graphics Adapter: igdumd64
 * Driver Version: 9.17.10.4229
 * Scaling Factor: 1
一月 11, 2018 12:32:11 下午 com.jme3.renderer.opengl.GLRenderer loadCapabilitiesCommon
信息: OpenGL Renderer Information
 * Vendor: Intel
 * Renderer: Intel(R) HD Graphics
 * OpenGL Version: 3.1.0 - Build 9.17.10.4229
 * GLSL Version: 1.40 - Intel Build 9.17.10.4229
 * Profile: Compatibility
一月 11, 2018 12:32:11 下午 com.jme3.asset.AssetConfig loadText
一月 11, 2018 12:32:12 下午 com.jme3.audio.openal.ALAudioRenderer initOpenAL
信息: Audio Renderer Information
 * Device: OpenAL Soft
 * Vendor: OpenAL Community
 * Renderer: OpenAL Soft
 * Version: 1.1 ALSOFT 1.15.1
 * Supported channels: 64
 * ALC extensions: ALC_ENUMERATE_ALL_EXT ALC_ENUMERATION_EXT ALC_EXT_CAPTURE ALC_EXT_DEDICATED ALC_EXT_disconnect ALC_EXT_EFX ALC_EXT_thread_local_context ALC_SOFT_loopback
 * AL extensions: AL_EXT_ALAW AL_EXT_DOUBLE AL_EXT_EXPONENT_DISTANCE AL_EXT_FLOAT32 AL_EXT_IMA4 AL_EXT_LINEAR_DISTANCE AL_EXT_MCFORMATS AL_EXT_MULAW AL_EXT_MULAW_MCFORMATS AL_EXT_OFFSET AL_EXT_source_distance_model AL_LOKI_quadriphonic AL_SOFT_buffer_samples AL_SOFT_buffer_sub_data AL_SOFTX_deferred_updates AL_SOFT_direct_channels AL_SOFT_loop_points AL_SOFT_source_latency
一月 11, 2018 12:32:12 下午 com.jme3.audio.openal.ALAudioRenderer initOpenAL
信息: Audio effect extension version: 1.0
一月 11, 2018 12:32:12 下午 com.jme3.audio.openal.ALAudioRenderer initOpenAL
信息: Audio max auxiliary sends: 4
Hello jMonkeyEngine!
</code></pre>
<p>相比之下，继承SimpleApplication之后，控制台只是额外多输出了一些启动信息。最终还是打印了一行 <code>Hello jMonkeyEngine!</code>，证明文件读取成功了。</p>
<p>前后两个代码没有实质区别。只是从 <code>main()</code> 方法挪到了 <code>simpleInitApp()</code> 里面。如果你愿意，还可以定义一个类来专门进行文件操作，然后在 TestReadFile 类中调用它。与普通的Java程序没有任何区别。</p>
<h4 id="assetmanager">使用AssetManager</h4>
<p>使用 AssetManager，代码稍微有一点变化：</p>
<pre><code>    @Override
    public void simpleInitApp() {
        // 注册运行时当前目录
        assetManager.registerLocator(&quot;./&quot;, FileLocator.class);
        try {
            // 打开文件
            AssetInfo info = assetManager.locateAsset(new AssetKey(&quot;assets/hello.txt&quot;));
            InputStream in = info.openStream();
    
            // 读取文件
            Scanner scanner = new Scanner(in);
            String line = scanner.nextLine();
            System.out.println(line);
            
            // 关闭文件
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
</code></pre>
<p>因为要从当前目录中加载资源，所以把 <code>&quot;./&quot;</code> 目录和 <code>FileLocator.class</code> 注册到了 AssetManager 中。然后通过 <code>locateAsset()</code> 方法得到 <code>AssetInfo</code> 对象。</p>
<p>如果把 <code>assets</code> 目录注册到 AssetManager 中，那么文件的路径需要从 <code>assets/hello.txt</code> 缩短为 <code>hello.txt</code>。</p>
<pre><code>    @Override
    public void simpleInitApp() {
        // 注册运行时当前目录
        assetManager.registerLocator(&quot;assets&quot;, FileLocator.class);
        try {
            // 打开文件
            AssetInfo info = assetManager.locateAsset(new AssetKey(&quot;hello.txt&quot;));
            InputStream in = info.openStream();
    
            // 读取文件
            Scanner scanner = new Scanner(in);
            String line = scanner.nextLine();
            System.out.println(line);
            
            // 关闭文件
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
</code></pre>
<h3 id="">小结</h3>
<p>本章进一步分析了 AssetLocator 的原理及用法，下一章我们来尝试实现自定义 AssetLocator。</p>
]]></content:encoded></item><item><title><![CDATA[第十六章：AssetManager接口介绍]]></title><description><![CDATA[<p>下面的文章，是我早期阅读jME3源代码时整理的笔记，主要介绍了 AssetManager 中的主要接口。结合上一篇文章，可以深入了解AssetManager的具体用法。</p>
<h3 id="">核心组件介绍</h3>
<h4 id="assetmanager">AssetManager</h4>
<p>这是JME3资源管理器的核心接口，它提供了统一的方式来管理各种资源。</p>
<p>(1) 注册资源加载器</p>
<pre><code>public void registerLoader(Class loaderClass, String ... extensions)
</code></pre>
<p>根据后缀名来注册资源加载器。没有注册过的资源类型是无法被AssetManager识别的。<br>
例：</p>
<pre><code>assetManager.registerLoader(AWTLoader.class, &quot;jpg&quot;);
assetManager.registerLoader(WAVLoader.class, &quot;wav&quot;);
</code></pre>
<p>(2) 注册资源定位器</p>
<pre><code>public void registerLocator(String rootPath, Class locatorClass)
</code></pre>
<p>注册资源位置，以及定位器。加载资源的时候，AssetManager会到注册过的位置来查找资源。</p>]]></description><link>https://blog.jmecn.net/interfaces-of-asset-manager/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b4a</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Tue, 09 Jan 2018 12:44:19 GMT</pubDate><content:encoded><![CDATA[<p>下面的文章，是我早期阅读jME3源代码时整理的笔记，主要介绍了 AssetManager 中的主要接口。结合上一篇文章，可以深入了解AssetManager的具体用法。</p>
<h3 id="">核心组件介绍</h3>
<h4 id="assetmanager">AssetManager</h4>
<p>这是JME3资源管理器的核心接口，它提供了统一的方式来管理各种资源。</p>
<p>(1) 注册资源加载器</p>
<pre><code>public void registerLoader(Class loaderClass, String ... extensions)
</code></pre>
<p>根据后缀名来注册资源加载器。没有注册过的资源类型是无法被AssetManager识别的。<br>
例：</p>
<pre><code>assetManager.registerLoader(AWTLoader.class, &quot;jpg&quot;);
assetManager.registerLoader(WAVLoader.class, &quot;wav&quot;);
</code></pre>
<p>(2) 注册资源定位器</p>
<pre><code>public void registerLocator(String rootPath, Class locatorClass)
</code></pre>
<p>注册资源位置，以及定位器。加载资源的时候，AssetManager会到注册过的位置来查找资源。例：</p>
<pre><code>assetManager.registerLocator(&quot;/&quot;, ClasspathLocator.class);
assetManager.registerLocator(&quot;res/models.zip&quot;, ZipLocator.class);
</code></pre>
<p>(3) 定位资源位置</p>
<pre><code>public AssetInfo locateAsset(AssetKey&lt;?&gt; key)
</code></pre>
<p>这个方法将根据AssetKey中的路径，按顺序遍历所有注册过的资源位置，直到查询到了一个匹配的资源为止。</p>
<p>如果找到了资源，就会返回一个AssetInfo对象，否则将返回null。<br>
AssetInfo对象包含了资源的InputStream，我们可以直接解析资源数据，也可以通过AssetLoader来加载资源。</p>
<p>(4) 加载资源</p>
<pre><code>public &lt;T&gt; T loadAsset(AssetKey&lt;T&gt; key);
</code></pre>
<p>这个方法用于加载资源，具体的加载过程我们后面再详细分析。<br>
AssetKey中包含了资源的后缀名，若该资源类型的AssetLoader没有在AssetManager中注册过，程序就会抛出异常。</p>
<h4 id="assetkey">AssetKey</h4>
<p>AssetKey是用来从缓存中寻找资源的钥匙，可以使用资源路径来构造一个AssetKey。</p>
<pre><code>AssetKey = new AssetKey(&quot;Common/MatDefs/Misc/Unshaded.j3md&quot;);
</code></pre>
<p>一旦资源路径设置完成后，它的值就无法改变了，因为AssetKey没有提供任何方法来修改资源路径。<br>
AssetKey会自动帮我们计算资源的后缀名、文件夹。</p>
<p>(1) 资源全路径</p>
<pre><code>public String getName()
</code></pre>
<p>返回资源的全路径。</p>
<pre><code>&quot;Common/MatDefs/Misc/Unshaded.j3md&quot;
</code></pre>
<p>(2) 资源文件夹</p>
<pre><code>public String getFolder()
</code></pre>
<p>返回资源所在的文件夹。</p>
<pre><code>&quot;Common/MatDefs/Misc/&quot;
</code></pre>
<p>(3) 资源后缀名<br>
后缀名不分大小写。</p>
<pre><code>public String getExtension()
</code></pre>
<p>例：</p>
<pre><code>&quot;j3md&quot;
</code></pre>
<p>(4) 资源的缓存类型</p>
<pre><code>public Class&lt;? extends AssetCache&gt; getCacheType()
</code></pre>
<p>资源加载的同时，会在缓存中保存一份，防止直接被GC回收。<br>
AssetKey默认使用SimpleCacheType，这意味着直接使用JME3自带的AssetKey的话，我们就需要自己手动去释放缓存。。</p>
<p>(5) 资源加载后的处理器</p>
<pre><code>public Class&lt;? extends AssetProcessor&gt; getProcessorType()
</code></pre>
<p>默认为null<br>
jpg、tga等图片资源作为纹理加载时，首先会变成一个Image对象。通过TextrueProcesser处理后才会变成一个程序中所需要的Texture对象。</p>
<h4 id="assetlocator">AssetLocator</h4>
<p>AssetLocator是一个接口，用于从指定位置查询资源信息。</p>
<p>(1)资源根目录</p>
<pre><code>public void setRootPath(String rootPath)
</code></pre>
<p>资源定位器允许我们在指定一个资源加载的根路径。<br>
定位资源的时候，调用AssetKey的getName()方法可以获得资源在这个根目录中的相对位置。</p>
<p>举个例子：注册一个ZipLocatoer，设置资源根目录为&quot;res/models.zip&quot;。查找资源&quot;img/avatar.png&quot;的时候，这个ZipLocatoer就会在models.zip文件找去查询img/avatar.png文件。</p>
<p>(2)定位资源</p>
<pre><code>public AssetInfo locate(AssetManager manager, AssetKey key)
</code></pre>
<p>在AssetLocator定位了资源位置后，将会返回一个AssetInfo对象。</p>
<h4 id="assetinfo">AssetInfo</h4>
<p>AssetInfo是AssetLocater定位资源后返回的结构，其中提供了指定资源的InputStream。</p>
<p>(1)资源数据</p>
<pre><code>public abstract InputStream openStream();
</code></pre>
<p>AssetInfo是一个抽象类，调用openStream()方法即可获得资源的InputStream，通过这个InputStream就可以读取实际的资源数据了。</p>
<p>(2)getKey</p>
<pre><code>public AssetKey getKey()
</code></pre>
<p>通过这个方法可以获得资源的AssetKey</p>
<p>(3)getManager</p>
<pre><code>public AssetManager getManager()
</code></pre>
<p>通过这个方法可以获得加载该资源的AssetManager</p>
<h4 id="assetloader">AssetLoader</h4>
<p>AssetLoader用于加载指定类型的资源，资源类型通过文件的后缀名来匹配。<br>
AssetLoader接口中只有一个用于加载的接口：</p>
<pre><code>public Object load(AssetInfo assetInfo) throws IOException;
</code></pre>
<p>AssetLoader将调用AssetInfo的openStream()方法来获得资源的输入流，并将数据解析成一个我们所需要的对象。</p>
<h3 id="assetloaderassetlocator">AssetLoader和AssetLocator</h3>
<p>AssetManager加载资源前，首先要注册各种AssetLoader和AssetLocator，否则AssetManager将不知道怎么去加载资源。</p>
<h4 id="assetloader">注册AssetLoader</h4>
<p>AssetLoader由AssetManger管理，加载资源时，AssetManager通过后缀名来匹配AssetLoader。<br>
例如：</p>
<pre><code>assetManager.loadAsset(&quot;Interfaces/background.jpg&quot;);
</code></pre>
<p>当这段代码执行时，assetManager会根据后缀名&quot;jpg&quot;去查找AssetLoader实例。如果AssetManager中没有匹配&quot;jpg&quot;后缀的AssetLoader，那么这个资源就在加载不了了。幸好JME3中自带了一些常用类型的资源加载器，并且默认在启动时就给它们注册了，如下：</p>
<pre><code>assetManager.registerLoader(AWTLoader.class, &quot;jpg&quot;);  
</code></pre>
<p>AssetManager使用 <strong>Map&lt;String, AssetLoader&gt;</strong> 来保存资源后缀名与AssetLoader之间的映射管理。如果2个AssetLoader都注册了同样的后缀名，那么后注册的AssetLoader会挤掉先定义的AssetLoader。</p>
<h4 id="assetlocator">注册AssetLocator</h4>
<p>JME3允许你从不同的位置加载资源，诸如：</p>
<ul>
<li>Classpath： assets.jar</li>
<li>压缩包：assets.zip</li>
<li>文件夹：MyGame/Pictures</li>
<li>URL：<a href="http://yourhost/assets/">http://yourhost/assets/</a></li>
</ul>
<p>这些都是通过AssetLocator实现的。</p>
<p>初学JME3时，我们一般是在项目中新建一个assets源码文件夹(source folder)，然后在这个文件夹下创建Interface、Model、Material等包(package)，然后再需要使用的资源放在这些目录下。加载这些资源时，其实是ClasspathLocater在起作用。</p>
<p>如果你不满足于这种模式，想使用另外地方的资源，那么可以尝试如下的方式：</p>
<pre><code>assetManager.registerLocator(&quot;res&quot;, FileLocator.class);//注册程序相对路径res
assetManager.registerLocator(&quot;C:/&quot;, FileLocator.class);// 注册绝对路径(Windows)
assetManager.registerLocator(&quot;/usr/yan/myassets/&quot;, FileLocator.class);// 注册绝对路径(Linux)
assetManager.registerLocator(&quot;/&quot;, ClasspathLocator.class);  
</code></pre>
<p>AssetManager把所有注册过的AssetLocator保存在一个 <strong>List&lt;AssetLocater&gt;</strong> 中，使用时按从前往后的顺序查找。如果2个AssetLocator都能找到相同路径的资源，那么先注册的AssetLocator会被使用，后面的会被忽略。</p>
<h4 id="">默认注册</h4>
<p>JME3 Desktop应用启动时，默认注册了下面这些AssetLoader和AssetLocater。</p>
<pre><code>LOCATOR / com.jme3.asset.plugins.ClasspathLocator
LOADER com.jme3.texture.plugins.AWTLoader : jpg, bmp, gif, png, jpeg
LOADER com.jme3.audio.plugins.WAVLoader : wav
LOADER com.jme3.audio.plugins.OGGLoader : ogg
LOADER com.jme3.cursors.plugins.CursorLoader : ani, cur, ico
LOADER com.jme3.material.plugins.J3MLoader : j3m
LOADER com.jme3.material.plugins.J3MLoader : j3md
LOADER com.jme3.material.plugins.ShaderNodeDefinitionLoader : j3sn
LOADER com.jme3.font.plugins.BitmapFontLoader : fnt
LOADER com.jme3.texture.plugins.DDSLoader : dds
LOADER com.jme3.texture.plugins.PFMLoader : pfm
LOADER com.jme3.texture.plugins.HDRLoader : hdr
LOADER com.jme3.texture.plugins.TGALoader : tga
LOADER com.jme3.export.binary.BinaryImporter : j3o
LOADER com.jme3.export.binary.BinaryImporter : j3f
LOADER com.jme3.scene.plugins.OBJLoader : obj
LOADER com.jme3.scene.plugins.MTLLoader : mtl
LOADER com.jme3.scene.plugins.ogre.MeshLoader : meshxml, mesh.xml
LOADER com.jme3.scene.plugins.ogre.SkeletonLoader : skeletonxml, skeleton.xml
LOADER com.jme3.scene.plugins.ogre.MaterialLoader : material
LOADER com.jme3.scene.plugins.ogre.SceneLoader : scene
LOADER com.jme3.scene.plugins.blender.BlenderModelLoader : blend
LOADER com.jme3.shader.plugins.GLSLLoader : vert, frag, glsl, glsllib  
</code></pre>
<p>上面的数据来自JME3 Desktop自带的配置文件：com/jme3/asset/Desktop.cfg</p>
<p>对于Android来说，JME3 Android启动时还加载了drawable文件夹和asset文件夹的Locator，具体是哪些类就不列出来了。</p>
<h4 id="assetconfig">AssetConfig</h4>
<p>除了在代码里面直接调用AssetManager的方法来注册，我们还可以利用配置文件来进行注册。</p>
<p>配置文件的格式就和上面的代码一样，AssetConfig类专门用于解析这种配置文件。然而实际上我们在编程的时候几乎不上AssetConfig，只要注意配置的方式就行了。我们这里主要谈谈怎么使用配置文件。</p>
<p>配置文件的使用有3个关键点：</p>
<ol>
<li>配置文件必须放在工程的classpath之下，否则无法识别。</li>
<li>要在AppSettings中添加参数&quot;AssetConfigURL&quot;，指定配置文件的加载路径。</li>
<li>如果使用自定义配置文件，那么jme3默认的配置文件就不会生效了！</li>
</ol>
<p>你可以在自己的资源目录夹下面创建一个cfg文件，格式和内容可参考JME3自带的cfg文件，然后采用如下方式在程序启动时加载它：</p>
<pre><code>public static void main(String[] args) {
    AppSettings settings = new AppSettings(false);
    // 设置文件路径
    settings.set(&quot;AssetConfigURL&quot;, &quot;your/asset/path/Assets.cfg&quot;);
    // 启动程序
    SimpleApplication game = new MyGame();
    game.setSettings(settings);
    game.start();
}  
</code></pre>
<p>AssetManager初始化时，会从AppSettings中读取&quot;AssetConfigURL&quot;这个参数，然后再读取配置文件。如果找不到配置文件的话，就会使用默认配置文件。源码如下：</p>
<pre><code>private void initAssetManager(){
    if (settings != null){
        String assetCfg = settings.getString(&quot;AssetConfigURL&quot;);
        if (assetCfg != null){
            URL url = null;
            try {
                url = new URL(assetCfg);
            } catch (MalformedURLException ex) {
            }
            if (url == null) {
                url = Application.class.getClassLoader().getResource(assetCfg);
                if (url == null) {
                    logger.log(Level.SEVERE, &quot;Unable to access AssetConfigURL in asset config:{0}&quot;, assetCfg);
                    return;
                }
            }
            assetManager = JmeSystem.newAssetManager(url);
        }
    }
    if (assetManager == null){
        assetManager = JmeSystem.newAssetManager(
                Thread.currentThread().getContextClassLoader().getResource(&quot;com/jme3/asset/Desktop.cfg&quot;));
    }
}
</code></pre>
<h3 id="">资源加载流程</h3>
<p>JME3在加载资源的过程中，AssetManager会先根据AssetKey去缓存中查找资源，如果找得到的话就直接使用，找不到的话才会去AssetLocator注册的路径下搜索。</p>
<p>具体加载的流程是这样的：</p>
<h4 id="">在缓存中查找资源</h4>
<p>检查AssetCache中的资源，若找不到就进行下一步，若找到就直接返回了。</p>
<pre><code>AssetCache cache = handler.getCache(key.getCacheType());
Object obj = cache != null ? cache.getFromCache(key) : null;
</code></pre>
<h4 id="assetloader">匹配AssetLoader</h4>
<p>根据资源后缀名来匹配AssetLoader，找不到的话抛出异常。</p>
<pre><code>AssetLoader loader = handler.aquireLoader(key);
</code></pre>
<h4 id="">资源定位</h4>
<p>遍历所有注册过的AssetLocater，返回AssetInfo，找不到的话会抛出异常。</p>
<pre><code>AssetInfo info = handler.tryLocate(key);
</code></pre>
<h4 id="">加载资源</h4>
<p>调用AssetLoader的load(AssetInfo info)方法，返回资源对象。</p>
<pre><code>obj = loader.load(info);
</code></pre>
<h4 id="">后续处理</h4>
<p>AssetLoader返回Object类型的对象，经过AssetProcessor处理后，转换成实际的对象类型。<br>
比如AWTLoader读取图片数据后，返回Image类型的对象。再通过TextureProcessor处理后才变成Texture对象。</p>
<pre><code>AssetProcessor proc = handler.getProcessor(key.getProcessorType());
if (proc != null){
    // do processing on asset before caching
    obj = proc.postProcess(key, obj);
}
</code></pre>
<h4 id="">保存到缓存</h4>
<p>资源加载结束后，对象会保存到缓存中。</p>
<pre><code>if (cache != null){
    // At this point, obj should be of type T
    cache.addToCache(key, (T) obj);
}</code></pre>
]]></content:encoded></item><item><title><![CDATA[第十五章：AssetManager工作流程]]></title><description><![CDATA[<h3 id="">基本过程</h3>
<p>jME3 使用Java I/O来读取资产数据，把整个过程分为三个步骤：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/asset_manager_process.png" alt=""></p>
<p><strong>一、打开输入流</strong></p>
<p>这一步通常会根据资源路径（URL）获得一个 InputStream 对象，程序可以通过这个对象来读取数据。输入流的来源可能多种多样，例如：</p>
<ul>
<li>文件路径 new FileInputStream(&quot;D:\MyGame\Save\savedata.dat&quot;)</li>
<li>网络地址 socket.getInputStream();</li>
<li>内存地址 new ByteArrayInputStream(data);</li>
<li>等等..</li>
</ul>
<p>不同来源的输入流，需要有不同的处理方法。在 jME3 中，这一步由不同类型的 <code>AssetLocator</code> 负责完成。例如，通过下面的代码可以让 AssetManager 从磁盘文件夹中加载资产。</p>
<pre><code>assetManager.registerLocator(&quot;F:</code></pre>]]></description><link>https://blog.jmecn.net/asset-manager-working-flow/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b49</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Tue, 09 Jan 2018 12:36:14 GMT</pubDate><content:encoded><![CDATA[<h3 id="">基本过程</h3>
<p>jME3 使用Java I/O来读取资产数据，把整个过程分为三个步骤：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/asset_manager_process.png" alt=""></p>
<p><strong>一、打开输入流</strong></p>
<p>这一步通常会根据资源路径（URL）获得一个 InputStream 对象，程序可以通过这个对象来读取数据。输入流的来源可能多种多样，例如：</p>
<ul>
<li>文件路径 new FileInputStream(&quot;D:\MyGame\Save\savedata.dat&quot;)</li>
<li>网络地址 socket.getInputStream();</li>
<li>内存地址 new ByteArrayInputStream(data);</li>
<li>等等..</li>
</ul>
<p>不同来源的输入流，需要有不同的处理方法。在 jME3 中，这一步由不同类型的 <code>AssetLocator</code> 负责完成。例如，通过下面的代码可以让 AssetManager 从磁盘文件夹中加载资产。</p>
<pre><code>assetManager.registerLocator(&quot;F:\Models&quot;, FileLocator.class);
</code></pre>
<p>当使用 assetManager 来加载路径为 <code>Models/Monkey/monkey.j3o</code> 的资产时，<code>FileLocator</code> 就会试图通过下面这种方式来打开输入流：</p>
<pre><code>InputStream is = new FileInputStream(&quot;F:\Models\Models\Monkey\monkey.j3o&quot;);
</code></pre>
<p>等 <code>AssetLocator</code> 把输入流搞定后，具体如何读取数据，就会交给第二步来处理。</p>
<p><strong>二、解析数据</strong></p>
<p>游戏中会用到很多不同类型的多媒体资产，需要根据文件格式来解析。</p>
<p>例如jpeg、png、gif等图片文件，先要根据压缩算法对图像数据进行解压，然后恢复成实际的图像数据才能在游戏中使用。</p>
<p>例如fbx、blend、3ds等模型文件，其中的内容可能由上千个不同的数据结构组成，需要针对每一种结构来分别处理，最终得到正确的顶点、法线、纹理坐标等数据来使用。</p>
<p>这些资产数据解析后，一般要生成 jME3 能够识别的专有数据结构，然后才能使用。在 jME3 中，这一步由不同类型的 <code>AssetLoader</code> 负责完成。例如 TGALoader 负责解析 tga 图片，生成 <code>Texture</code> 对象；OBJLoader 负责解析 obj 模型，生成 <code>Spatial</code> 对象。</p>
<p>jME3实现了多种不同的 <code>AssetLoader</code>，通过下面的代码，可以让 AssetManager 记住文件格式与 <code>AssetLoader</code> 之间的对应关系。</p>
<pre><code>assetManager.registerLoader(TGALoader.class, &quot;.tga&quot;);
assetManager.registerLoader(DDSLoader.class, &quot;.dds&quot;);
assetManager.registerLoader(OBJLoader.class, &quot;.obj&quot;);
assetManager.registerLoader(MTLLoader.class, &quot;.mtl&quot;);
</code></pre>
<p>AssetManager 会自动根据 <strong>文件后缀名</strong> 来判断数据格式，找到对应的 AssetLoader 来解析数据。</p>
<p>jME3 专门定义了 <code>AssetKey</code> 类，它的主要作用是保存资产的路径（URL），并分析其后缀名。例如：</p>
<pre><code>AssetKey key = new AssetKey(&quot;Models/Monkey/monkey.j3o&quot;);
assert key.getName() == &quot;Models/Monkey/monkey.j3o&quot;;
assert key.getFolder() == &quot;Models/Monkey/&quot;;
assert key.getExtension() == &quot;j3o&quot;;
</code></pre>
<p>等找到了正确的 <code>AssetLoader</code> ，解析完毕后，输入流就可以关掉了。</p>
<p><strong>三、关闭输入流</strong></p>
<p>这是最简单的，通常只需要调用 InputStream 的 close() 方法即可。</p>
<h3 id="">缓存</h3>
<p>在进行游戏开发时，很多资产文件都会被重复使用。比如路人NPC的模型，在一个场景中可能会出现很多次。又比如噪声纹理，可能被当做“云”材质使用，也可能被当做“水”材质使用。如果每次使用某个资产时都要从I/O加载，那引擎的性能就太低了。一般的游戏引擎都会缓存游戏资产。</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/asset_manager_cache.png" alt=""></p>
<p>缓存通常是内存中的Hash表，以 &lt;Key,Value&gt; 的形式保存资产。 AssetManager 使用 <code>HashMap</code> 来缓存资产，并定义了 <code>AssetKey</code> 用来查找和缓存资产。</p>
<p>AssetManager 在打开输入流之前，先要创建一个 <code>AssetKey</code>，并使用它来查找缓存。由于资产的路径（URL）通常是唯一的，jME3默认使用资产的URL来创建 AssetKey。</p>
<p>AssetManager 以 AssetKey 对象作为参数，查找 HashMap 中的资产。结果分为两种：</p>
<ul>
<li><strong>命中缓存</strong>：如果发现需要使用的是内存中已经存在的资产，那就直接在内存中 <strong>复制（克隆）</strong> 一份。这样就不需要走I/O流了，速度会快很多。</li>
<li><strong>未命中缓存</strong>：如果在HashMap中没有找到资产，那么就走I/O流程。数据解析完毕后，把AssetKey和资产都 <strong>存入HashMap中</strong>，等下次使用就可以直接加载了。</li>
</ul>
<p>这个缓存机制可以极大地减少 I/O 操作，但也带来了一些负面影响：</p>
<ul>
<li>热更新失效：如果在程序外部修改了资源内容，希望游戏内能看到改变，但由于资产的URL没变，导致 AssetManager 使用了缓存而不是更新后的文件。</li>
<li>垃圾回收失效：Java的内存回收是依赖于底层 GC 的。当 HashMap 中一直保存这对象的引用时，GC 永远不会回收此对象的内存。</li>
</ul>
<p>这两个问都好解决， AssetManager 中提供了 <code>deleteFromCache(AssetKey key)</code> 方法，可以手动清除缓存；也可以通过 AssetKey 设置不同的缓存机制，这个后续的文章再详细介绍。</p>
<h3 id="">文件之间的关联</h3>
<p>使用前面介绍的模式来加载资产，流程固然清晰，但也带来了其它问题。</p>
<p>假设 AssetLocator 接口和 AssetLoader 接口是这样定义的：</p>
<pre><code>public interface AssetLocator {
    public void setRootPath(String rootPath);
    public InputStream locate(AssetKey key);
}

public interface AssetLocator {
    public Object load(InputStream in) throws IOException;
}
</code></pre>
<p>若 OBJLoader 正在加载一个 obj 文件，其中记录了材质文件（.mtl）的相对路径。必须要需要根据 .obj 文件的 URL 来计算 .mtl 文件的路径，否则就无法加载模型的材质。可是在上面设计的接口中，AssetLoader 只拿到了 InputStream ，而 <strong>InputStream 是不保存资源路径的。</strong> 这就导致无法加载 .mtl 文件。</p>
<p>这就像一个老板，分别交代甲和乙合作完成一件事，但是禁止甲和乙私下沟通。现在乙要做一件事，可这件事只有甲才知道怎么办，乙就懵逼了。</p>
<p>jME3的接口当然没有这种低级错误，它使用 <code>AssetInfo</code> 来解决了这个问题。解决方法很简单，就是把 <code>AssetManager</code>、<code>AssetKey</code>、<code>InputStream</code> 等东西都保存在 AssetInfo 对象中，然后一起传给 AssetLoader。</p>
<p><code>AssetInfo</code> 的定义如下：</p>
<pre><code>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() + &quot;[&quot; + &quot;key=&quot; + key + &quot;]&quot;;
    }
    public abstract InputStream openStream();
}
</code></pre>
<p>其中的主要方法如下：</p>
<ul>
<li><code>openStream()</code> 方法，返回 <code>InputStream</code> 对象，用于解析数据；</li>
<li><code>getKey()</code> 方法，返回 <code>AssetKey</code> 对象，通过它就能够得到资产路径；</li>
<li><code>getManager()</code> 方法，返回 <code>AssetManager</code> 对象，可以用它来加载相关的纹理、材质等资产。</li>
</ul>
<p>jME3 中实际的 AssetLocator 和 AssetLoader 接口设计是这样的：</p>
<pre><code>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;
}
</code></pre>
<h3 id="">小结</h3>
<p>经过前面的介绍，下面重新梳理一下AssetManager的工作流程。</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/asset_manager_assetinfo.png" alt=""></p>
<p>具体的资源加载步骤如下：</p>
<ol>
<li>根据资产的URL，创建AssetKey。</li>
<li>根据AssetKey去缓存中查找资源。</li>
<li>若AssetKey命中缓存，则复制资源内容，直接返回。</li>
<li>若AssetKey未命中，各种AssetLocator将去根目录下搜索对应的URL。若找不到资产，则抛出 AssetNotFoundException。</li>
<li>若AssetLocator找到资源，则打开输入流，输出AssetInfo对象。</li>
<li>AssetLoader根据输入的AssetInfo，解析资源数据。</li>
<li>将解析结果保存到缓存。</li>
<li>关闭输入流。</li>
</ol>
<p>需要注意的是，上述流程并不完整。在第6步和第7步之间，实际上还有一个<strong>后期处理</strong>过程。因为AssetLoader返回的对象有时并不能直接被游戏引擎使用。</p>
<p>比如各种图片文件，解析的结果是一个<code>Image</code>对象，但游戏需要使用的是<code>Texture</code>对象。jME3 设计了 AssetProcessor 接口，用于对 AssetLoader 生成的对象进行后期处理，转换成实际的对象类型。</p>
<p>但是AssetProcessor在整个流程中的存在感很弱，一般情况下可以当它不存在，所以我就不仔细介绍了。在以后的文章中，如果需要用到 AssetProcessor，我再详细分析。</p>
]]></content:encoded></item><item><title><![CDATA[第十四章：Java I/O流]]></title><description><![CDATA[<p>有一个笑话是这样的：</p>
<blockquote>
<p>问：要把大象装冰箱，一共分几步？<br>
答：分三步，一把冰箱门打开，二把大象放进去，三把冰箱门关上。</p>
</blockquote>
<p>这个笑话的无厘头之处在于，正常人都明白把大象放进冰箱是最难的，而打开、关上冰箱门根本不算事。回答的人却一本正经地把它们放在一起说，仿佛把大象放进冰箱是像打开冰箱门一样简单的问题。</p>
<p>Java 的 I/O 流也分三步，一打开I/O流，二读/写数据，三关闭I/O流。与开头的笑话恰恰相反，I/O操作最复杂的是第一步。相比之下，读写数据几乎不算事。</p>
<p>复杂的主要原因，是Java在设计时对文件读写进行了高层抽象，统一为InputStream 和 OutputStream。这个设计的初衷是想统一文件、网络、设备、内存等不同来源的I/O操作，但是却忽略了实际每一种I/O操作都有特殊的需求，导致I/O包下的继承层次尤其复杂。</p>
<p>jME3 的资产管理是建立在 Java 的 I/</p>]]></description><link>https://blog.jmecn.net/java-iostream/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b48</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Sun, 07 Jan 2018 07:57:47 GMT</pubDate><content:encoded><![CDATA[<p>有一个笑话是这样的：</p>
<blockquote>
<p>问：要把大象装冰箱，一共分几步？<br>
答：分三步，一把冰箱门打开，二把大象放进去，三把冰箱门关上。</p>
</blockquote>
<p>这个笑话的无厘头之处在于，正常人都明白把大象放进冰箱是最难的，而打开、关上冰箱门根本不算事。回答的人却一本正经地把它们放在一起说，仿佛把大象放进冰箱是像打开冰箱门一样简单的问题。</p>
<p>Java 的 I/O 流也分三步，一打开I/O流，二读/写数据，三关闭I/O流。与开头的笑话恰恰相反，I/O操作最复杂的是第一步。相比之下，读写数据几乎不算事。</p>
<p>复杂的主要原因，是Java在设计时对文件读写进行了高层抽象，统一为InputStream 和 OutputStream。这个设计的初衷是想统一文件、网络、设备、内存等不同来源的I/O操作，但是却忽略了实际每一种I/O操作都有特殊的需求，导致I/O包下的继承层次尤其复杂。</p>
<p>jME3 的资产管理是建立在 Java 的 I/O 机制之上的。考虑不少学习使用jME3的用户都是刚学习Java不久的人，我认为有必要对 Java 的 I/O 流知识做一些介绍，避免一些读者在阅读后续章节时可能产生的不适感。</p>
<p>但我不准备把本文写成Java I/O教程，而是假设读者有Java I/O的概念，至少会用 FileInputStream 来读文件。在这个基础上，我会分析 InputStream 和 OutputStream 最核心的用法，再进一步阐释用 I/O 来读取游戏资产时可能需要用到的方法。</p>
<p>如果读者的Java I/O基础比较好，可以直接跳过本章去看后面的内容。如果读者觉得本章内容太长，也可以直接跳到后面的文章。</p>
<p>等读到自己不太理解的内容时再回过头来看这一章，也许才能有所体会。</p>
<h3 id="">输入流</h3>
<h4 id="">示例代码</h4>
<p>先贴一段简单的代码。</p>
<pre><code>public static void main(String[] args) {
    String result = null;
    
    try {
        // 打开输入流
        InputStream in = new FileInputStream(&quot;index.html&quot;);
        
        int len = in.available();
        System.out.println(&quot;总字节数: &quot; + len);
        
        // 读取数据
        byte[] data = new byte[len];
        in.read(data, 0, len);
        
        len = in.available();
        System.out.println(&quot;剩余字节: &quot; + len);
        
        result = new String(data);
        
        // 关闭输入流
        in.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
    
    // 打印结果
    System.out.println(result);
}
</code></pre>
<p>这是一段常见的Java读文件代码。它从磁盘上读取了一个名为 index.html 文件，并把数据转成字符串输出到控制台。(该文件是从 <a href="https://github.com/jmecn/jmecn.github.io/blob/master/index.html">https://github.com/jmecn/jmecn.github.io/blob/master/index.html</a> 复制的。)</p>
<p>运行结果：</p>
<pre><code>总字节数: 10958
剩余字节: 0
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;zh-CN&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
    &lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&gt;
    &lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1&quot;&gt;
...略
</code></pre>
<h4 id="">代码分析</h4>
<p>这段代码可以分解成三个阶段：</p>
<p><strong>1.打开文件</strong></p>
<pre><code>        // 打开输入流
        InputStream in = new FileInputStream(&quot;index.html&quot;);
</code></pre>
<p><strong>2.读文件</strong></p>
<pre><code>        int len = in.available();
        System.out.println(&quot;总字节数: &quot; + len);
        
        // 读取数据
        byte[] data = new byte[len];
        in.read(data, 0, len);
        
        len = in.available();
        System.out.println(&quot;剩余字节: &quot; + len);
</code></pre>
<p><strong>3.关闭文件</strong></p>
<pre><code>        // 关闭输入流
        in.close();
</code></pre>
<p>打开文件和关闭文件分别只有一行代码，就不多解释了。分析一下读文件的代码，会发现其实只使用了 InputStream 的 <code>in.available();</code> 和 <code>in.read(data, 0, len);</code> 这两个方法。</p>
<p><code>available()</code> 方法的作用是告知我们输入流中还有多少数据可读。假设文件的长度是 <strong>10958</strong> 字节，调用 <code>read(data, 0, 1024)</code> 方法读取了 <strong>1024</strong> 字节后，就还剩 <strong>9934 = 10958 - 1024</strong> 个字节可读。</p>
<p>对于文件输入流（FileInputStream）来说，如果在调用 <code>read()</code> 方法前先调用 <code>available()</code> 方法，由于此时文件数据还没有被读取过，<code>available()</code> 返回的就是文件的总长度。</p>
<pre><code>        int len = in.available();
        System.out.println(&quot;总字节数: &quot; + len);
</code></pre>
<p>根据 len 的值，就知道需要分配多少内存来存储文件数据。</p>
<pre><code>        // 读取数据
        byte[] data = new byte[len];
</code></pre>
<p>然后调用 <code>read()</code> 方法读取数据，保存到数组 <code>data</code> 中。</p>
<pre><code>        in.read(data, 0, len);
</code></pre>
<p>再次调用 <code>available()</code> 方法，可以获得文件中剩余的字节数。</p>
<pre><code>        len = in.available();
        System.out.println(&quot;剩余字节: &quot; + len);
</code></pre>
<p>拿到数据后，就可以转成字符串输出。</p>
<pre><code>    result = new String(data);
    // 打印结果
    System.out.println(result);
</code></pre>
<h4 id="">分段读取</h4>
<p>上面的代码是不是很简单？**然而这个代码是有缺陷的，**最大的缺陷是忽略了文件的大小。</p>
<p>通常情况下，计算机的硬盘空间会比内存大很多。假如程序要读取的是一个3GB大小的文件，而Java虚拟机只有2GB内存，上面的代码运行时就会...Booooom...内存炸了。</p>
<p>更好的做法是把数据切成可控的“小块”来读取，一边读取一边使用。例如循环读取1KB数据，直到文件数据被全部读完为止。</p>
<p>改造例一中的读文件代码：</p>
<pre><code>        int len = in.available();
        System.out.println(&quot;总字节数: &quot; + len);

        StringBuffer sb = new StringBuffer();
        
        // 读取数据
        len = 0;
        byte[] data = new byte[1024];// 1KB
        while( (len = in.read(data, 0, 1024)) != -1) {
            sb.append(new String(data));
            System.out.printf(&quot;读取: %d 剩余: %d\n&quot;, len, in.available());
        }
        
        result = sb.toString();
</code></pre>
<p>运行结果：</p>
<pre><code>总字节数: 10958
读取: 1024 剩余: 9934
读取: 1024 剩余: 8910
读取: 1024 剩余: 7886
读取: 1024 剩余: 6862
读取: 1024 剩余: 5838
读取: 1024 剩余: 4814
读取: 1024 剩余: 3790
读取: 1024 剩余: 2766
读取: 1024 剩余: 1742
读取: 1024 剩余: 718
读取: 718 剩余: 0
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;zh-CN&quot;&gt;
  &lt;head&gt;
    &lt;meta charset=&quot;utf-8&quot;&gt;
...略
</code></pre>
<p>这段代码并没有解决我刚才说的“内存爆炸”问题，文件数据全部都被写到了 <code>StringBuffer</code> 中，该炸还是会炸。但这只是因为我懒得处理数据的取出，不是重点，重点是这一段代码。</p>
<pre><code>        // 读取数据
        len = 0;
        byte[] data = new byte[1024];// 1KB
        while( (len = in.read(data, 0, 1024)) != -1) {
            sb.append(new String(data));
            System.out.printf(&quot;读取: %d 剩余: %d\n&quot;, len, in.available());
        }
</code></pre>
<p>根据我多年的经验，这是Java学生在学I/O流时最头疼的一段代码。首先要搞明白这段代码做了什么，然后再来分析它是怎么做的。</p>
<p>这段代码的作用，就是我刚才说的：<strong>把数据切成可控的“小块”来读取，一边读取一边使用。</strong></p>
<p>首先，分配了一个固定大小的字节数组，作为读数据的缓存。</p>
<pre><code>        byte[] data = new byte[1024];// 1KB
</code></pre>
<p>然后，在循环中读取数据，每次读1KB大小，直到读完为止。</p>
<pre><code>        while( (len = in.read(data, 0, 1024)) != -1) {
            sb.append(new String(data, 0, len));
            System.out.printf(&quot;读取: %d 剩余: %d\n&quot;, len, in.available());
        }
</code></pre>
<p>要看懂这个代码，必须理解 <code>read</code> 方法具体是怎么工作的。</p>
<p><code>read(byte b[], int off, int len)</code> 方法的作用是从输入流中读取 len 个字节，并把数据写入到字节数组b中，并返回实际读取了多少数据。如果没有读取到任何数据，意味着文件已经读取完毕，返回 -1。</p>
<p>如果 len 的值是 10958，而字节数组b的长度是 1024，显然不可能把全部数据都写入到 b 中，最多只能读取 1024 字节，此时 read() 方法的返回值是 1024。参数len的值为1024时，就是希望每次读取1024个字节。</p>
<p>这是控制台中的输出：</p>
<pre><code>总字节数: 10958
读取: 1024 剩余: 9934
读取: 1024 剩余: 8910
读取: 1024 剩余: 7886
读取: 1024 剩余: 6862
...
</code></pre>
<p>当输入流中的可用数据不足 len 个字节时会怎么样呢？就像我买了一根11米长的绳子，打算剪成3米长的跳绳，总会有一段绳子不足3米长吧？</p>
<p>观察下面的输出。</p>
<pre><code>读取: 1024 剩余: 1742
读取: 1024 剩余: 718
读取: 718 剩余: 0
</code></pre>
<p>当文件被多次读取后，最后还剩 718 字节，不足1KB。此时 data 中只读取了 718 个字节，<code>read()</code> 方法返回实际读取字节数。</p>
<p>在 while 循环中， <code>len = in.read(data, 0, 1024)</code> 语句的作用是<strong>尝试</strong>从输入流中读取1KB数据，保存到数组data中，len 记录了实际读取的字节数。如果可用数据不足1KB，就只读取可用的数据。</p>
<p>因为实际读取的字节数可能比数组data的长度小，所以不能直接把整个数据中的全部数据当做<strong>有效数据</strong>去使用。这时候 len 的作用就体现出来了，它约束了实际使用的字节数量。</p>
<pre><code>        sb.append(new String(data, 0, len));
</code></pre>
<p><code>(len = in.read(data, 0, 1024))</code>是一个赋值表达式，返回的是len的值。</p>
<p><code>(len = in.read(data, 0, 1024)) != -1</code> 是一个逻辑表达式，作用是判断 len 的值是否为 -1。根据前面的介绍，len 等于 -1 时，说明文件已经读完了，就应该终止循环。</p>
<p>所以这个while循环内干了好几件事：一、从输入流读取1KB数据；二、用len记录实际读取了多少字节；三、根据len 的值是否为-1来判断文件是否读完了。</p>
<pre><code>while ((len = in.read(data, 0, 1024)) != -1) {
    // ..
}
</code></pre>
<h4 id="">下标偏移</h4>
<p><code>read(byte b[], int off, int len)</code> 方法的第二个参数 <code>off</code>，是一个下标偏移量(offset)。有时候，可能数组b中已经存储了一部分数据，我们不希望把它给覆盖了。这时候就不能从数组下标0开始读取数据，需要一个下标偏移量。</p>
<p>这就像两个油漆工合作刷一面墙，双方约定好一个刷蓝色，另一个刷红色。在开工之前应该约定互相工作的范围。不能一个人先把墙刷蓝了，另一个人再把他刷过的地方涂成红色，那这面墙就脏了。</p>
<p>例如要分别读2个文件，写到同一个数组b中。文件1的长度是666，文件2的长度是233，数组b的总长度应该是899。</p>
<p>假如代码像下面这样写，就会出问题：</p>
<pre><code>in1.read(b, 0, 666);
in2.read(b, 0, 233);
</code></pre>
<p>b 先从 <code>in1</code> 中读取了 666 个字节，然后又从 <code>in2</code> 中读取了 233 个字节。第二行代码执行时，先读取的 666 字节前面被覆盖了 233 个字节。</p>
<p>第二次读取数据时，应该从第一次读取后的位置继续读取，而不是从0处开始读取。</p>
<pre><code>in1.read(b, 0, 666);
in2.read(b, 666, 233);
</code></pre>
<p>假设定义字节数组 <code>byte[] data = new byte[1024]</code>，执行方法 <code>len = in.read(data, 233, 1024);</code>，<code>len</code> 的值是多少呢？</p>
<p>这个方法的含义，是<strong>尝试</strong>从输入流中读取1024字节数据，保存到数组data中，从数组下标233处开始保存。由于data的长度是1024，从下标233到下标1023处一共只剩下 801 = 1024-233 字节，因此最多只能读取801字节。如果输入流可用字节数少于801，最终能够读取到的数据会更少。</p>
<h4 id="">读取用户输入</h4>
<p>我们都很熟悉 <code>System.in</code>，它是Java系统内置的输入流，功能是读取控制台输入。</p>
<pre><code>public static void main(String[] args) {
    
    try {
        // 打开输入流
        InputStream in = System.in;
        // 打开输出流
        OutputStream out = System.out;
        // 读取数据
        int len = 0;
        byte[] data = new byte[1024];// 1KB
        while( (len = in.read(data, 0, 1024)) != -1) {
            // 写数据
            out.write(data, 0, len);
            System.out.printf(&quot;读取: %d 剩余: %d\n&quot;, len, in.available());
        }
        // 关闭输入流
        // in.close();
        
    } catch (IOException e) {
        e.printStackTrace();
    }
}
</code></pre>
<p>这段代码在运行时，会阻塞在 <code>in.read(data, 0, 1024)</code> 这一行，因为系统会等待用户的输入。我在控制台中输入的结果如下：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/sys_in.png" alt=""></p>
<p>注意，System.in与文件输入不一样，它不存在“读取完毕”这回事。只要用户有输入，它就会拿到输入的数据；如果用户不输入，代码就会阻塞在 <code>read()</code> 方法处等待用户输出，因此这是一个死循环。</p>
<p>实际使用时，一般不会这样写在while循环内。想获得用户输入，可以使用一些工具类，例如 java.util.Scanner。</p>
<pre><code>        Scanner input = new Scanner(System.in);
        // 读取并输出一行字符串
        System.out.println(input.nextLine());
</code></pre>
<p>还有一点需要注意的是，不要把 System.in 当做普通的InputStream去调用 close() 方法。这样干的结果是<strong>当前程序</strong>再也无法通过它获得用户输入了。</p>
<h4 id="">读取网络数据</h4>
<p>在示例代码中，我演示了如何从磁盘文件读取数据。事实上 <a href="http://www.jmecn.net">http://www.jmecn.net</a> 网站的首页和该文件的内容是一模一样的，可否直接读取网络数据呢？</p>
<p>可以，而且实现非常简单。只要修改“打开文件”，部分的代码。</p>
<p>原来的输入流是磁盘上的文件：</p>
<pre><code>        // 打开输入流
        InputStream in = new FileInputStream(&quot;index.html&quot;);
</code></pre>
<p>现在改成网络连接：</p>
<pre><code>        URL url = new URL(&quot;http://www.jmecn.net/index.html&quot;);
        HttpURLConnection conn = (HttpURLConnection)url.openConnection();
        
        if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
            System.out.println(&quot;网络打开失败!&quot;);
            return;
        }
        
        // 打开输入流
        InputStream in = conn.getInputStream();
</code></pre>
<p>剩下的代码没有任何区别。</p>
<h3 id="">输出流</h3>
<h4 id="">写文件</h4>
<p>在上一节中，我把读取来的数据写到了一个 StringBuffer 中。如果文件超大的话，内存依然会爆炸。实际开发中，一般不会这么做。</p>
<p>正常的做法，要么是从源头限制文件的大小，不要读取超大文件；要么是根据需要来对数据进行实时处理，最常见的就是数据拷贝。</p>
<p>比如我可以把 <code>index.html</code> 的内容写到一个 <code>index2.html</code> 里，就相当于拷贝了一个文件。</p>
<p>对实例代码中的“读文件”部分做一点点改动，把 StringBuffer 换成文件输出流。</p>
<pre><code>        int len = in.available();
        System.out.println(&quot;总字节数: &quot; + len);
        
        // 打开输出流
        OutputStream out = new FileOutputStream(&quot;index2.html&quot;);
        // 读取数据
        len = 0;
        byte[] data = new byte[1024];// 1KB
        while( (len = in.read(data, 0, 1024)) != -1) {
            // 写数据
            out.write(data, 0, len);
            System.out.printf(&quot;读取: %d 剩余: %d\n&quot;, len, in.available());
        }
        // 关闭输出流
        out.close();
</code></pre>
<p>这段代码的作用就变成了<strong>复制</strong>文件，把 index.html 复制到 index2.html。</p>
<h4 id="">输出到控制台</h4>
<p>事实上，我们常用的 <code>System.out</code> 本身也是一个输出流，只不过它是输出到控制台了。如果把 OutputStream 指向 System.out，结果就会在控制台中输出这个文件的内容。</p>
<pre><code>        int len = in.available();
        System.out.println(&quot;总字节数: &quot; + len);
        
        // 打开输出流
        OutputStream out = System.out;
        // 读取数据
        len = 0;
        byte[] data = new byte[1024];// 1KB
        while( (len = in.read(data, 0, 1024)) != -1) {
            // 写数据
            out.write(data, 0, len);
            System.out.printf(&quot;读取: %d 剩余: %d\n&quot;, len, in.available());
        }
        // 关闭输出流
        //out.close();
</code></pre>
<h4 id="">输出到网络</h4>
<p>Java 的网络通信同样是基于I/O流的。对于网络服务器来说，通过InputStream可以获得用户的输入，通过OutputStream可以把数据发给客户端。</p>
<p>继续在示例代码的基础上加功能。创建一个ServerSocket来监听80端口（HTTP协议的默认端口），有任何客户端连接时，就把index.html的内容发送给它。</p>
<pre><code>public static void main(String[] args) {
    
    try {
        // 启动Socket服务器，监听80端口。
        ServerSocket server = new ServerSocket(80);
        
        while(true) {
            Socket client = server.accept();
            
            // 打开输入流
            InputStream in = new FileInputStream(&quot;index.html&quot;);
            
            int len = in.available();
            System.out.println(&quot;总字节数: &quot; + len);
            
            // 打开输出流
            OutputStream out = client.getOutputStream();
            // 读取数据
            len = 0;
            byte[] data = new byte[1024];// 1KB
            while( (len = in.read(data, 0, 1024)) != -1) {
                // 写数据
                out.write(data, 0, len);
                System.out.printf(&quot;读取: %d 剩余: %d\n&quot;, len, in.available());
            }
            client.close();// 断开客户链接
            
            // 关闭输入流
            in.close();
        }
        
    } catch (IOException e) {
        e.printStackTrace();
    }
    
}
</code></pre>
<p>这一段代码的改变主要是增加了一个死循环，因为服务器是持续运行的。当然正常情况下不会这样写死循环，一般都会有结束线程的方式，还会用连接池+线程池来处理并发访问。但我们这只是一个非常简单的例子，就不要纠结这种问题了。</p>
<p>在死循环中，通过 <code>Socket client = server.accpet();</code> 来等待客户端访问。这个操作是阻塞的，如果没有客户访问，代码就会停在这里一直等待。当有客户链接时，就会读取 index.html 文件的内容，并通过 <code>client.getOutputStream()</code> 发送给客户。发送完毕之后立即执行 <code>client.close()</code> 方法断开链接，等待下一个客户链接。</p>
<pre><code>            // 打开输出流
            OutputStream out = client.getOutputStream();
            // 读取数据
            len = 0;
            byte[] data = new byte[1024];// 1KB
            while( (len = in.read(data, 0, 1024)) != -1) {
                // 写数据
                out.write(data, 0, len);
                System.out.printf(&quot;读取: %d 剩余: %d\n&quot;, len, in.available());
            }
            client.close();// 断开客户链接
</code></pre>
<p>运行程序，打开浏览器，输入 <a href="http://127.0.0.1/">http://127.0.0.1/</a> 来访问本机服务器。不出意外你就会看到一个网页。</p>
<p>实际的HTTP服务器并不会这样简单得返回同一个页面，通常还会根据用户发送来的Http报文做一系列解析，然后调用服务端程序（Servlet/PHP等）来处理请求，但基本原理如此。</p>
<h3 id="">数据格式</h3>
<p>前面的文章大致介绍了基于 InputStream 和 OutputStream 读写字节数据的方法。但实际应用中会遇到一些更细节的问题，例如：</p>
<ul>
<li>我要读取3D模型文件，其中有浮点数、字符串、结构体等不同类型的二进制数据，应该如何用Java来解析？</li>
<li>我要读取CAD、点云（PT）等纯文本格式的数据，应该如何解析？</li>
</ul>
<p>本节将介绍如何处理不同类型的数据格式。</p>
<h4 id="">字节序</h4>
<p>在介绍具体数据类型的读取方法之前，请容我先介绍 <strong>字节序（Byte Order）</strong> 的概念。这并不是因为本人太啰嗦，而是因为字节顺序在读取数据时非常重要。</p>
<p><strong>字节序的来历</strong></p>
<p>谈到字节序的问题，必然牵涉到两大CPU派系。那就是PowerPC系列CPU和x86系列CPU。PowerPC系列采用big endian方式存储数据，而x86系列则采用little endian方式存储数据。那么究竟什么是big endian，什么又是little endian呢？</p>
<p>**端模式（Endian）**的这个词出自 Jonathan Swift 的小说《格列佛游记》。这本书根据将鸡蛋敲开的方法不同将所有的人分为两类：从圆头开始将鸡蛋敲开的人被归为Big-Endian，从尖头开始将鸡蛋敲开的人被归为Littile-Endian。小人国的内战就源于吃鸡蛋时是究竟从 <strong>大头(Big-Endian)</strong> 敲开还是从 <strong>小头(Little-Endian)</strong> 敲开，由此曾发生过六次叛乱，其中一个皇帝送了命，另一个丢了王位。</p>
<p>在计算机业Big-Endian和Little-Endian也几乎引起一场战争。在计算机业界，Endian表示数据在存储器中的存放顺序。采用 <strong>大端</strong> 方式进行数据存放 <strong>符合人类的正常思维</strong> ，而采用 <strong>小端</strong> 方式进行数据存放 <strong>利于计算机处理</strong>。</p>
<p><strong>大端的优势</strong></p>
<p>什么叫做低序？什么叫高位？为什么说<strong>采用大端方式进行数据存放符合人类的正常思维</strong>？</p>
<p>例如：一个十进制的阿拉伯数字 <code>1234</code> ，它表示一千二百三十四，书写顺序是 1、2、3、4。这就是大端(Big-Endian)字序。</p>
<ul>
<li>数字 <code>1</code> 是 <code>1234</code> 中的最高位（千），书写时排在第1（最低序）；</li>
<li>数字 <code>4</code> 是 <code>1234</code> 中的最低位（个位），书写顺序排在第4（最高序）。</li>
</ul>
<p>如果有人把“一千二百三十四”写成 <code>4321</code>，就叫小端（Little-Endian）。是不是感觉很反人类？</p>
<p>假设计算机中有一个无符号整数（4字节） 0x12345678，在内存中占4个字节。在大端、小端两种模式的计算机中，分别是这样存放的：</p>
<p>Big-endian：将高序字节存储在起始地址（高位编址）</p>
<pre><code>低地址                                            高地址
----------------------------------------------------&gt;
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     12     |      34    |     56      |     78    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
</code></pre>
<p>Little-endian：将低序字节存储在起始地址（低位编址）</p>
<pre><code>低地址                                            高地址
----------------------------------------------------&gt;
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     78     |      56    |     34      |     12    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
</code></pre>
<p>相比之下，按大端字节序存储的数据，是不是更符合人类的习惯？</p>
<p><strong>小端的优势</strong></p>
<p>Little-Endian的存在，主要是计算方便。</p>
<p>举一个例子，在计算表达式 <code>14 * 37</code> 时，要从个位开始，逐个计算到高位。</p>
<pre><code>    14            14            14
  x  7          x 30          x 37
  ----          ----          ----
    28           120            98
  + 7           +30            420
  ----          ----          ----
    98           420           518
</code></pre>
<p>无论是乘法还是加法，计算时都是从低位开始的，并且逐渐累加进位。因此，把低位数存放在低序位置，优先参与计算，是一个很自然的事情。</p>
<p>其次，按Little-Endian存储时，1位(byte)、2位(short)、4位(int)、8位(long)等不同长度的整数，计算顺序都是一样的。而且还非常便于类型转换，只需要从低位开始截取就行了。</p>
<p><strong>学习I/O操作为什么要了解字节序？</strong></p>
<p>不同的操作系统、不同的编程语言，产生的文件格式是不一样的。当我们写的程序要处理其他平台、其他语言产生的数据时，就需要注意字节序的转换。否则就可能会出现 <code>1234</code> 被当做 <code>4321</code> 来处理的情况。</p>
<p>所有网络协议都是采用Big-Endian的方式来传输数据的，所以有时也会把Big-Endian方式称之为网络字节序。当两台采用不同字节序的主机通信时，在发送数据之前都必须经过字节序的转换成为网络字节序后再进行传输。</p>
<p>Java程序默认是以 Big-Endian 字节序来存储和读取数据的，而C/C++程序存储的字节序则与编译平台的CPU有关。大部分在Windows平台（x86的CPU）开发的C/C++程序，产生的文件都是 Little-Endian 字节序的。具体来说，如果你打算用Java程序来解析3ds格式的模型文件，如果忽略了字节序，恐怕得到的所有数据都是错误的。</p>
<h4 id="">整数</h4>
<p><strong>byte</strong></p>
<p>读取byte类型的数据最简单，直接调用 InputStream 的 read() 方法即可。</p>
<pre><code>public byte readByte(InputStream in) throws IOException {
    byte b = in.read();
    return b;
}
</code></pre>
<p>需要注意的是， <strong>Java中的byte、short、int、long都是有符号数，不支持unsigned类型</strong> 。为了保证读取字节时符号正确， <code>in.read()</code> 实际上返回的是一个 int 类型的数值。上面的代码等价于：</p>
<pre><code>public byte readByte(InputStream in) throws IOException {
    int n = in.read();
    return (byte)n;
}
</code></pre>
<p>通常情况下，整数 n 的取值为 0~255。如果输入流中已经没有可读取的字符， read() 方法将返回 -1。为了防止输入流已经结束，应该在代码中加入 (n &lt; 0) 的判断。若输入流已经结束，则抛出文件结束异常（End of File Exception）。</p>
<pre><code>public byte readByte(InputStream in) throws IOException {
    int n = in.read();
    if (n &lt; 0)
        throws new EOFException();
    return (byte)n;
}
</code></pre>
<p><strong>unsigned byte</strong></p>
<p><strong>再说一遍，Java不支持无符号（unsigned）类型。</strong> Java使用<strong>补码</strong>来表示带符号的数值。如果你把这个方法返回的byte类型当做数值来处理，它的取值范围将是 -128~127。</p>
<pre><code>public static void main(String[] args) {
    byte a = (byte)0x80;
    System.out.println(a);
}
</code></pre>
<p>输出为： <code>-128</code>。</p>
<pre><code>public static void main(String[] args) {
    byte a = (byte)0xFF;
    System.out.println(a);
}
</code></pre>
<p>输出为： <code>-1</code></p>
<p>如果你读取字节数据只是用于存储图片数据，或者用于转换成字符串，前面写的 readByte() 方法没有任何问题。但如果你把它的返回值当做 unsigned byte 来计算，可能就会出现错误。</p>
<p>对于带符号整数。Java的处理方式是使用额外的字节来存储。比如把byte读取为int，把int读取为long。虽然浪费内存，但也没有更好的处理办法了。</p>
<pre><code>public int readUnsignedByte(InputStream in) throws IOException {
    int n = in.read();
    if (n &lt; 0)
        throw new EOFException();
    return n;
}
</code></pre>
<p><strong>short</strong></p>
<p>Java中的short由2个字节组成，可以连续调用2次 in.read() 方法，然后把结果转成一个 short 类型。转换时，需要注意字节序。</p>
<p>对于Little-Endian来说，应该这样处理：</p>
<pre><code>int a = in.read();
int b = in.read();
short s = (short)((a&lt;&lt;0) + (b&lt;&lt;8));
</code></pre>
<p>对于Big-Endian来说，应该这样处理：</p>
<pre><code>int a = in.read();
int b = in.read();
short s = (short)((a&lt;&lt;8) + (b&lt;&lt;0));
</code></pre>
<p>Java默认是按Big-Endian方式来读取数据的，方法如下：</p>
<pre><code>public short readShort(InputStream in) throws IOException {
    int a = in.read();
    int b = in.read();
    if ((a | b) &lt; 0)
        throw new EOFException();
    return (short)((a &lt;&lt; 8) + (b &lt;&lt; 0));
}
</code></pre>
<p>Java中short类型的取值范围是 -32768 ~ 32767。</p>
<p><strong>unsigned short</strong></p>
<p><strong>说第三遍，Java不支持无符号（unsigned）类型。</strong> 如果想要读取 unsigned short 类型，通常的做法是把它升格为 int 类型。</p>
<pre><code>public int readUnsignedShort(InputStream in) throws IOException {
    int a = in.read();
    int b = in.read();
    if ((a | b) &lt; 0)
        throw new EOFException();
    return (a &lt;&lt; 8) + (b &lt;&lt; 0);
}
</code></pre>
<p>这个方法的返回值，取值范围是 0 ~ 65535</p>
<p><strong>int</strong></p>
<p>int类型使用4个字节来表示。按 Big-Endian 字节序读取，是这样的：</p>
<pre><code>public int readInt(InputStream in) throws IOException {
    int ch1 = in.read();
    int ch2 = in.read();
    int ch3 = in.read();
    int ch4 = in.read();
    if ((ch1 | ch2 | ch3 | ch4) &lt; 0)
        throw new EOFException();
    return ((ch1 &lt;&lt; 24) + (ch2 &lt;&lt; 16) + (ch3 &lt;&lt; 8) + (ch4 &lt;&lt; 0));
}
</code></pre>
<p>按 Little-Endian 字节序读取，是这样的：</p>
<pre><code>public int readInt(InputStream in) throws IOException {
    int ch1 = in.read();
    int ch2 = in.read();
    int ch3 = in.read();
    int ch4 = in.read();
    if ((ch1 | ch2 | ch3 | ch4) &lt; 0)
        throw new EOFException();
    return ((ch1 &lt;&lt; 0) + (ch2 &lt;&lt; 8) + (ch3 &lt;&lt; 16) + (ch4 &lt;&lt; 24));
}
</code></pre>
<p>取值范围： -2147483648 ~ 2147483647。</p>
<p><strong>unsigned int</strong></p>
<p>与前面一样，对数据做升格处理。把 int 升格成 long 读取。</p>
<p>Big-Endian 版的代码：</p>
<pre><code>public long readUnsignedInt(InputStream in) throws IOException {
    int ch1 = in.read();
    int ch2 = in.read();
    int ch3 = in.read();
    int ch4 = in.read();
    if ((ch1 | ch2 | ch3 | ch4) &lt; 0)
        throw new EOFException();
    return (long)(((long)(ch1 &amp; 0xFF) &lt;&lt; 24) + (ch2 &lt;&lt; 16) + (ch3 &lt;&lt; 8) + (ch4 &lt;&lt; 0));
}
</code></pre>
<p>注意，由于只有最高位字节可能带符号，因此要将 ch1 先转成long型，然后再进行算数左移。</p>
<p>Little-Endian 版的代码我就不写了。做法是一样的，只是换了高位、低位的顺序。</p>
<p><strong>long</strong></p>
<p>我想你应该已经明白是怎么回事了。无非就是读取8个字节，然后依字节顺序进行算术左移。</p>
<pre><code>private byte readBuffer[] = new byte[8];
public long readLong(InputStream in) throws IOException {
    if (in.read(readBuffer, 0, 8) &lt; 0)
        throw new EOFException();
    return (((long)readBuffer[0] &lt;&lt; 56) +
            ((long)(readBuffer[1] &amp; 0xFF) &lt;&lt; 48) +
            ((long)(readBuffer[2] &amp; 0xFF) &lt;&lt; 40) +
            ((long)(readBuffer[3] &amp; 0xFF) &lt;&lt; 32) +
            ((long)(readBuffer[4] &amp; 0xFF) &lt;&lt; 24) +
            ((readBuffer[5] &amp; 0xFF) &lt;&lt; 16) +
            ((readBuffer[6] &amp; 0xFF) &lt;&lt;  8) +
            ((readBuffer[7] &amp; 0xFF) &lt;&lt;  0));
}
</code></pre>
<p>同样，应该根据字节序来写具体的实现。如果是Little-Endian，就得把顺序颠倒过来。</p>
<h4 id="">浮点数</h4>
<p>浮点数有两种精度，分别是单精度（float）和双精度（double）。一般float类型采用4字节存储，double类型采用8字节存储。使用Java读取浮点数时，一般先把字节当做 int 和 long 型的整数读取，然后调用 Float 和 Double 类的静态方法，把对字节进行转换。</p>
<p><strong>float</strong></p>
<p>Java的Float类提供了静态方法 intBitsToFloat(int bits) 方法，可以把一个4字节的整数（int）转换为4字节的单精度浮点数（float）。</p>
<p>通过调用前面实现的 readInt() 方法，配合Float.intBitsToFloat() 方法，就可以从输入流中读取float。</p>
<pre><code>public float readFloat(InputStream in) throws IOException {
    return Float.intBitsToFloat(readInt(in));
}
</code></pre>
<p><strong>double</strong></p>
<p>Java的Double类同样提供了 longBitsToDouble 方法，可以把一个8字节的长整型（long）转成一个8字节的双精度浮点数（double）。</p>
<pre><code>public double readDouble(InputStream in) throws IOException {
    return Double.longBitsToDouble(readLong(in));
}
</code></pre>
<h4 id="">字符串</h4>
<p><strong>定长字符串</strong></p>
<p>在某些3D格式的文件中，为了尽量对齐数据结构，软件会把字符串固定为16字节、32字节、64字节、128字节等长度。解析这种文件时，需要注意字符串的实际<strong>有效长度</strong>。</p>
<p>假设一个字符串 &quot;Hello World!&quot;，实际长度是12字节，保存到固定16字节的内存中，多余的4字节通常会用 '\0' 来填充。</p>
<p>在从输入流中读取字符串时，我们并不知道实际长度到底是多少，因此通常会先把整个定长字符串读取到一个缓存中。读取之后，再从开头查找 '\0' 出现的位置，用来判断字符串的实际长度。最后把实际用到的字节数据传给String的构造方法，用来生成一个String对象。</p>
<pre><code>public String getString(InputStream in, int len) throws IOException {
    if (len &lt;= 0)
        return &quot;&quot;;

    byte[] buf = new byte[len];
    in.read(buf, 0, len);

    int i;
    for (i = 0; i &lt; len; i++) {
        if (buf[i] == 0)
            break;
    }

    if (i == 0)
        return &quot;&quot;;

    if (i == len)
        return new String(buf);

    return new String(buf, 0, i);
}
</code></pre>
<p><strong>变长字符串</strong></p>
<p>变长字符串比定长字符串常见得多，一般有两种存储格式：</p>
<ul>
<li>字符串内容+结束符 '\0' 。</li>
<li>字符串长度+字符串内容。</li>
</ul>
<p>前一种格式，每个学过C语言的人都不会陌生。C语言中通常会用一个 char 数组来保存字符串，字符串结尾以 <code>'\0'</code> 标识。</p>
<pre><code>char str[16] = {'H', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '\0'};
</code></pre>
<p>这种数据结构解析起来并不难，只要逐字节读取，遇到 <code>0x00</code> 就结束即可。</p>
<pre><code>public static String getString(InputStream in) throws IOException {
    StringBuffer stringBuffer = new StringBuffer();
    char charIn = (char) in.read();
    while (charIn != 0x00) {
        stringBuffer.append(charIn);
        charIn = (char) in.read();
    }
    return stringBuffer.toString();
}
</code></pre>
<p>后一种格式，常见于各种语言中的字符串类（string）。通常会先用2个字节（或4个字节）的无符号整数来存储字符串的长度，后面再保存字符串的实际内容，不需要用 <code>'\0'</code> 来结尾。</p>
<p>对于这种数据结构来说，先要调用前面实现的 <code>readUnsignedShort()</code> 或 <code>readInt()</code> 方法来获得字符串的长度，再调用 <code>getString(InputStream in, int len)</code> 方法来读取定长字符串即可。</p>
<p>具体的代码就不写了。</p>
<h5 id="">乱码原因</h5>
<p>现在我们使用的高级编程语言，最初都是美国人发明的，他们的母语是英语。英语是一种拼音语言，他们只需要存储小写字母（a~z）、大写字母（A~Z）、阿拉伯数字（0~9）以及各种运算符号就行了，合起来都不超过128个符号。就算给键盘上每个功能键都增加一个编码，还把扑克牌上的四种花色都算进去了，美国标准编码（ASCII）字符集也不超过256个符号，使用<strong>单字节</strong>就足以表达任意符号了。</p>
<p>当计算机走向世界之后，世界各国的人们都在尝试在计算机上显示自己国家的语言。由于汉语的文字数量远远超过ASCII编码的数量，单字节是无法满足我们的，至少也得用<strong>双字节</strong>表示。例如 GBK 编码中的 <code>0xCCCC</code> 就表示汉字“烫”。</p>
<p>但是字符编码这件事并没有一个统一的标准，比如中国就有GBK、CP936、GB2312、BIG5等不同的编码。英语也仅仅属于拉丁语系中的一环，还有法语、俄语、德语、意大利语。各个国家都用自家的标准，结果互联网时代沟通非常不便。如果一个英国人的终端没有安装GBK编码，那么他访问中文网站看到的就是一堆乱码。</p>
<p>UTF-8/UTF-16/Unicode编码应运而生，势要统一地球上所有符号编码。这时候双字节不够用了，Unicode采用了<strong>多字节</strong>编码。但是，即使有标准，也未必会有人买账。比如中国软件已经使用了多年的GBK编码，为什么非要换成UTF-8编码？微软为了满足中国大陆市场的需要，Windows操作系统中的中文编码默认就是GBK。</p>
<p>在GBK编码环境下打开UTF-8编码的文件，或者反过来在Unicode环境下读取GBK编码的字符串，会怎么样？</p>
<p>还能怎么样，乱码了呗！</p>
<p>有诗为证：</p>
<pre><code>手持两把锟斤拷，
口中疾呼烫烫烫。
脚踏千朵屯屯屯，
笑看万物锘锘锘。
</code></pre>
<p><strong>棍斤拷</strong></p>
<p>源于GBK字符集和Unicode字符集之间的转换问题。Unicode和老编码体系的转化过程中，肯定有一些字，用Unicode是没法表示的，Unicode官方用了一个占位符来表示这些文字，这就是：U+FFFD REPLACEMENT CHARACTER。那么U+FFFD的UTF-8编码出来，恰好是 '\xef\xbf\xbd'。如果这个'\xef\xbf\xbd'，重复多次，例如 '\xef\xbf\xbd\xef\xbf\xbd'，然后放到GBK/CP936/GB2312/GB18030的环境中显示的话，一个汉字2个字节，最终的结果就是：锟斤拷——锟(0xEFBF)，斤（0xBDEF），拷（0xBFBD）。</p>
<p><strong>烫烫烫屯屯屯</strong></p>
<p>在Visual Studio中的Debug模式下，如果声明一个变量，但是没有初始化，微软会给未初始化的内存复制为0xCC。给为初始化的内存赋0xCC是有原因的，0xCC其实是INT3中断指令，所以如果在Debug模式下试图去执行这块未初始化的内存的话就会中断程序。</p>
<p>但VS中调试器默认的字符集是MBCS，而在MBCS中0xCCCC正好就是中文中的“烫”，所以显示出来就都是“烫烫烫..”。如果是用分配堆的内存，会初始化成0xCD，0xCDCD在MBCS字符集中就是“屯屯屯..”。</p>
<p><strong>锘锘锘</strong></p>
<p>BOM 是 Byte Order Mark 的缩写。是UTF编码方案里用于标识编码的标准标记，在UTF-16里本来是<code>FF FE</code>，变成UTF-8就成了<code>EF BB BF</code>。这个标记是可选的，因为UTF8字节没有顺序，所以它可以被用来检测一个字节流是否是UTF-8编码的。</p>
<ul>
<li>锘 <code>EFBB</code></li>
<li>匡 <code>BFEF</code></li>
<li>豢 <code>BBBF</code></li>
</ul>
<p>出现这个问题肯定是你写网页的时候用了记事本 ，记事本在保存文件的时候把原本文件的编码改了记事本会默认保存为UTF-8的编码，而如果你原本网页是GBK编码的，就会出现乱码~BOM就是把一个Unicode保留字符U+FEFF，按照文件存储者的编码方式编码后，塞到文件内容的最前边。这样用不同的Unicode编码去解析文件头，就可以得知文件的编码方式和大小端顺序。结果就是文件头部多出来了两三个字节。</p>
<h5 id="">乱码处理</h5>
<p>我曾经想制作一个工具，用来解决所有乱码问题。思路是根据数据的特征来统计字符串原本是用什么编码格式，然后按照对应的格式来解码。后来我只搞明白了一点，就是很难通过数据的内容来判断字符到底是什么编码。</p>
<p>例如一串数据是这样的： <code>0xCC 0xCE 0x1F 0xBF 0x3C 0xBA</code>。它既可能是GBK编码，也可能是UTF-8编码，还有可能都不是。</p>
<p>因此所谓的乱码处理，其实是有前提条件的，就是已知数据的编码。如果不知道，那么就只能猜。在已知编码格式的前提下，Java 的 String 类为我们提供了根据字符编码来解析字符串的构造方法。</p>
<p>假设从输入流中读取了数据 byte[] data，用它来创建一个字符串，代码如下：</p>
<pre><code>String str = new String(data);
</code></pre>
<p>若确定其编码格式为 GBK ，代码如下：</p>
<pre><code>String str = new String(data, &quot;GBK&quot;);
</code></pre>
<p>若想同时指定data中的有效数据和字符编码，代码如下：</p>
<pre><code>String str = new String(data, offset, len, &quot;GBK&quot;);
</code></pre>
<p>如果你发现字符串是乱码，想要尝试进行转码处理。可以使用 getBytes() 方法拿到 String 中的原始字节数据，再试着重新构建字符串。比如把按 &quot;GBK&quot; 编码的字符串重新按 &quot;UTF-8&quot; 编码生成字符串。</p>
<pre><code>byte[] b = str.getBytes(&quot;GBK&quot;);
String str2 = new String(b, &quot;UTF-8&quot;);
</code></pre>
<p>在处理网络传输的字符串时，通常会使用这样一个代码：</p>
<pre><code>String str2 = new String(str.getBytes(&quot;ISO-8859-1&quot;), &quot;UTF-8&quot;);
</code></pre>
<p>它实际上是把数据按单字节编码取出，然后尝试转换成 UTF-8 编码。</p>
<h4 id="c">C语言结构体</h4>
<p>假设在C语言中定义结构体如下：</p>
<pre><code>typedef struct Vert {
    float x, y, z;
    float nx, ny, nz;
    float r, g, b, a;
    float u, v;
    struct Vert * next;
} Vertex, *LinkedVertex;
</code></pre>
<p>按上述方式定义顶点结构体，在C语言中只需要一行 <code>fwrite</code> 代码就可以保存到文件中。</p>
<pre><code>Vertex vert = {
    0.0, 0.0, 0.0,
    1.0, 0.0, 0.0,
    1.0, 1.0, 1.0, 1.0,
    0.0, 0.0,
    NULL
};

fwrite(&amp;vert, sizeof(Vertex), 1, fp);
</code></pre>
<p>对于Java来说，读取文件中存储的结构体要复杂很多，基本上只能按照结构体存储的顺序逐一读取成员。而且还要注意是Little-Endian还是Big-Endian。</p>
<p>对于这样的纯数据结构体，通常会定义一个与结构体相似的Java类。</p>
<pre><code>public class Vertex {
    public float x, y, z;
    public float nx, ny, nz;
    public float r, g, b, a;
    public float u, v;
    public Vertex next;
}
</code></pre>
<p>然后利用前面写过 readFloat() 等方法来读取数据。</p>
<pre><code>public Vertex readData(InputStream in) throws IOException {
    Vertex v = new Vertex();
    v.x = readFloat(in);
    v.y = readFloat(in);
    v.z = readFloat(in);
    v.nx = readFloat(in);
    v.ny = readFloat(in);
    v.nz = readFloat(in);
    v.r = readFloat(in);
    v.g = readFloat(in);
    v.b = readFloat(in);
    v.a = readFloat(in);
    v.u = readFloat(in);
    v.v = readFloat(in);

    readInt();// 读取指针
    v.next = null;
    return v;
}
</code></pre>
<p>注意，C/C++中的指针类型，通常会存储一个4字节的地址，因此可以用 <code>readInt()</code> 方法来拿到它的值。</p>
<p>由于指针存储的是C/C++程序运行时的内存地址，在Java程序中几乎没有什么用，所以通常不需要保存这个值。但在读取二进制数据时，也不能因为这个值无用而忽略它，所以单独调用了一次 <code>readInt()</code> 方法。</p>
<h3 id="">常用工具类</h3>
<p>前文介绍了使用 Java I/O 来解析二进制数据时一些常见的问题和方法，但事实上大部分开发者并不会使用这么“原始”的方法来写代码。Java 中有一些工具类能够帮我们处理这些问题，只有在工具类无法满足需求时，程序员才需要自己写这种代码。</p>
<h4 id="datainputstream">DataInputStream</h4>
<p>java.io.DataInputStream 是一个专门用来读取二进制数据的类，它在InputStream上进行了一层包装，实现了 readByte()、readShort()、readFloat() 等方法。</p>
<p>使用时很简单，把一个 InputStream 传给 DataInputStream 的构造方法即可。</p>
<pre><code>InputStream in = new FileInputStream(&quot;example.dat&quot;);
DataInputStream dis = new DataInputStream(in);
byte b = dis.readByte();
float f = dis.readFloat();
dis.close();
</code></pre>
<p>它的主要问题是默认使用 Big-Endian 方式读取数据，对于Little-Endian字节序的数据来说是灾难性的。要知道，大部分3D建模工具都是使用C/C++开发的，而且经常工作在Windows系统上，导致其文件存储的字节序都是Little-Endian。</p>
<p>解决办法很笨也很简单，前文我们已经了解了Little-Endian字节序的数据应该如何读取，自己实现一个Little-Endian 字节序的工具类就好了。</p>
<h4 id="littleendian">LittleEndian</h4>
<p><code>jme3-core.jar</code> 模块中已经实现了一个类似于 DataInputStream 的工具类，名为 <code>com.jme3.util.LittleEndian</code>。它的用法与 DataInputStream 一模一样，只是把字节序改成了 Little-Endian，免除了你自己写代码的工作量。</p>
<pre><code>InputStream in = new FileInputStream(&quot;example.dat&quot;);
LittleEndian le = new LittleEndian(in);
byte b = le.readByte();
float f = le.readFloat();
le.close();
</code></pre>
<p>LittleEndian 类的源码如下：</p>
<p><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/main/java/com/jme3/util/LittleEndien.java">https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-core/src/main/java/com/jme3/util/LittleEndien.java</a></p>
]]></content:encoded></item><item><title><![CDATA[第十三章：常见异常]]></title><description><![CDATA[<h3 id="assetloadexception">AssetLoadException</h3>
<p>加载资产时，程序一运行就产生了 <code>com.jme3.asset.AssetLoadException: No loader registered for type &quot;xxx&quot;</code> 异常</p>
<pre><code>严重: Uncaught exception thrown in Thread[jME3 Main,5,main]
com.jme3.asset.AssetLoadException: No loader registered for type &quot;&quot;
    at
com.jme3.asset.ImplHandler.aquireLoader(ImplHandler.java:200)
    at 
com.jme3.asset.</code></pre>]]></description><link>https://blog.jmecn.net/common-exceptions-when-loading-assets/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b47</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Thu, 04 Jan 2018 09:32:15 GMT</pubDate><content:encoded><![CDATA[<h3 id="assetloadexception">AssetLoadException</h3>
<p>加载资产时，程序一运行就产生了 <code>com.jme3.asset.AssetLoadException: No loader registered for type &quot;xxx&quot;</code> 异常</p>
<pre><code>严重: Uncaught exception thrown in Thread[jME3 Main,5,main]
com.jme3.asset.AssetLoadException: No loader registered for type &quot;&quot;
    at
com.jme3.asset.ImplHandler.aquireLoader(ImplHandler.java:200)
    at 
com.jme3.asset.DesktopAssetManager.loadLocatedAsset(DesktopAssetManager.java:255)
</code></pre>
<p>首先，请检查该资产文件是否是 jME3 所支持的格式。比如jME3不支持 <code>.max</code>，直接加载这种模型将产生上述异常。</p>
<p>其次，如果加载的是 jME3 所支持的源数据格式，可能是因为没有添加相关的依赖库。</p>
<ul>
<li><code>.blend</code> 依赖 <code>jme3-blender-{version}.jar</code></li>
<li><code>.fbx</code>、<code>.mesh.xml</code>、<code>.gltf</code> 依赖 <code>jme3-plugins-{version}.jar</code></li>
<li><code>.ogg</code> 依赖 <code>jme3-jogg-{version}.jar</code></li>
</ul>
<h3 id="assetnotfoundexception">AssetNotFoundException</h3>
<p>若资产文件不存在，或者使用了错误的资产路径，将会产生 AssetNotFoundException 。</p>
<pre><code>com.jme3.asset.AssetNotFoundException: Materials/Monkey/monkey.j3m
</code></pre>
<p>若资产文件存在，但资产路径忽略了大小写，同样会产生 AssetNotFoundException，并提示 <code>Asset name doesn't match requirements.</code>。</p>
<pre><code>com.jme3.asset.AssetNotFoundException: Asset name doesn't match requirements.
&quot;D:/WORKSPACES/jworkspace/MyGame/bin/Models/Monkey/monkey.j3o&quot; doesn't match &quot;Models/Monkey/Monkey.j3o&quot;
</code></pre>
<p>对比路径：</p>
<ul>
<li>Models/Monkey/<strong>m</strong>onkey.j3o</li>
<li>Models/Monkey/<strong>M</strong>onkey.j3o</li>
</ul>
<h3 id="nullpointerexception">NullPointerException</h3>
<p>游戏在SDK中运行正常，但是发布成单独的可执行文件后（.jar, .jnlp, .exe, .app）在运行就报错了。程序弹框提示 <code>Cannot locate resource: Scenes/town/main.scene</code>，并退出。</p>
<pre><code>com.jme3.asset.DesktopAssetManager loadAsset
WARNING: Cannot locate resource: Scenes/town/main.scene
com.jme3.app.Application handleError
SEVERE: Uncaught exception thrown in Thread[LWJGL Renderer Thread,5,main]
java.lang.NullPointerException
</code></pre>
<p>SDK的默认编译脚本在打包可执行文件时，只会复制 j3o 模型文件。用SDK把 .scene、.mesh.xml、.blend 等源数据模型转成 .j3o 即可。</p>
<pre><code>// TODO 更多异常待补充</code></pre>
]]></content:encoded></item><item><title><![CDATA[第十二章：自定义资产路径]]></title><description><![CDATA[<p>默认情况下，JME3 程序总是从 <code>MyGame/assets</code> 目录中加载资产。但这并不是唯一的选择，还可以在 AssetManager 中配置其他资产路径。</p>
<p>本文将介绍如何使用 jME3 提供的 AssetLocator 来配置自定义资产路径。后续章节中我会继续介绍 AssetLocator 的原理，并通过案例来演示如何实现从加密资源包中解析数据。在了解原理后，你可以根据具体的算法来实现自己的 AssetLocator。</p>
<h4 id="assetlocator">AssetLocator</h4>
<p>在介绍具体的做法之前，先考虑一下可能存在的情况：</p>
<ol>
<li>游戏允许用户上传自定义模型和皮肤，这些文件不可能打包到游戏的可执行文件中，应该怎么加载它们？</li>
<li>我们的项目计划使用内容分发网络（CDN）和云存储（cloud storage）服务来管理游戏资产，这样用户只需要下载一个很小的客户端。应该如何访问网络服务器上的游戏资产呢？</li>
<li>我想和魔兽世界一样把游戏资产打成一个加密资源包，避免用户篡改数据，同时避免同行窃取游戏资产。应该如何从这些加密资源包中加载数据呢？</li>
</ol>
<p>jME3 设计了抽象的<strong>资产定位器</strong>（AssetLocator），用于扩展支持上述所有情况。jME3的核心模块中提供了 5 种不同类型的 AssetLocator 实现。</p>]]></description><link>https://blog.jmecn.net/loading-assets-from-custom-paths/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b46</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Thu, 04 Jan 2018 09:26:05 GMT</pubDate><content:encoded><![CDATA[<p>默认情况下，JME3 程序总是从 <code>MyGame/assets</code> 目录中加载资产。但这并不是唯一的选择，还可以在 AssetManager 中配置其他资产路径。</p>
<p>本文将介绍如何使用 jME3 提供的 AssetLocator 来配置自定义资产路径。后续章节中我会继续介绍 AssetLocator 的原理，并通过案例来演示如何实现从加密资源包中解析数据。在了解原理后，你可以根据具体的算法来实现自己的 AssetLocator。</p>
<h4 id="assetlocator">AssetLocator</h4>
<p>在介绍具体的做法之前，先考虑一下可能存在的情况：</p>
<ol>
<li>游戏允许用户上传自定义模型和皮肤，这些文件不可能打包到游戏的可执行文件中，应该怎么加载它们？</li>
<li>我们的项目计划使用内容分发网络（CDN）和云存储（cloud storage）服务来管理游戏资产，这样用户只需要下载一个很小的客户端。应该如何访问网络服务器上的游戏资产呢？</li>
<li>我想和魔兽世界一样把游戏资产打成一个加密资源包，避免用户篡改数据，同时避免同行窃取游戏资产。应该如何从这些加密资源包中加载数据呢？</li>
</ol>
<p>jME3 设计了抽象的<strong>资产定位器</strong>（AssetLocator），用于扩展支持上述所有情况。jME3的核心模块中提供了 5 种不同类型的 AssetLocator 实现。</p>
<ul>
<li>com.jme3.asset.plugins.ClasspathLocator 用于加载 classpath 中的资产，包括 jar 文件。</li>
<li>com.jme3.asset.plugins.FileLocator 用于加载文件系统中的资产。</li>
<li>com.jme3.asset.plugins.HttpZipLocator 用于加载网站上的zip压缩包。</li>
<li>com.jme3.asset.plugins.UrlLocator 用于加载网络资产。</li>
<li>com.jme3.asset.plugins.ZipLocator 用于加载 zip 压缩包中的资产。</li>
</ul>
<p>通过调用 AssetManager 中的 <code>registerLocator(String rootPath, Class locatorClass)</code> 方法，即可注册资产路径。这个方法有两个参数：其一是<strong>资产的根目录</strong>，其二是 <strong>AssetLocator 类型</strong>。</p>
<h4 id="classpathlocator">ClasspathLocator</h4>
<p>jME3程序能够从项目的 <code>assets</code> 目录中加载资产，是因为 SDK 已经把 assets 目录配置成了项目的 classpath，并且在 JME3 程序初始化时就执行下列语句：</p>
<pre><code>assetManager.registerLocator(&quot;/&quot;, ClasspathLocator.class);
</code></pre>
<p>因此，AssetManager 才可以从 <code>assets</code> 目录中加载资产，还可以直接读取 <code>jme3-core.jar</code> 中的内置资产。</p>
<p>回顾 <a href="http://blog.jmecn.net/manage-assets-in-other-ides/">第五章：在其他IDE中管理资产</a>，我在 Eclipse 中把 <code>assets</code> 目录配置成了 <code>Build Path</code>，在 IDEA 中把 <code>assets</code> 目录配置成了 <code>Resources Root</code>，实质上就是把它们添加到项目的 classpath 中，这样 AssetManager 就可以通过 ClasspathLocator 加载这些目录下的资产。</p>
<p>对于 Maven、Gradle 等项目来说， <code>src/main/resources</code> 也属于 classpath，因此存放其中的资产可以被加载。</p>
<h5 id="jar">手动打包jar文件</h5>
<p>如果把资产打包成 <code>assets.jar</code> 文件，并其添加到项目的依赖库中，这样 ClasspathLocator 还可以直接加载 <code>assets.jar</code> 中的资产文件。</p>
<p>做法很简单：<code>jar</code> 文件本身使用的 <code>zip</code> 压缩，先把资源打包成 <code>zip</code> 文件，然后把后缀名改成 <code>jar</code> 即可。</p>
<p><strong>注意：只压缩 <code>assets</code> 目录内的文件，不要压缩 <code>assets</code> 目录本身。</strong></p>
<p>压缩好的 <code>assets.zip</code> 文件结构如下：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/assets_zip.png" alt=""></p>
<p>把后缀名改成 jar，然后添加到项目的依赖库中。下图是在 Eclispe + Gradle 环境中的配置截图。</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/assets_jar.png" alt=""></p>
<p>然后就可以在 JME3 使用 <code>Models/Monkey/monkey.j3o</code> 加载模型， ClasspathLocator 会在幕后为我们做好一切工作。</p>
<p>一般来说，资产打包是由编译脚本自动化执行的。本文只是演示其原理，实际开发时请不要这样手动打包。</p>
<h4 id="filelocator">FileLocator</h4>
<p>FileLocator 可以直接访问操作系统的文件目录，调用 AssetManager 的 <code>registerLocator</code> 方法来注册资产根目录即可。</p>
<p>例如：游戏资产保存在 <code>F:\assets</code> 目录中，目录结构如下：</p>
<pre><code>F:\assets\Interface
F:\assets\MatDefs
F:\assets\Materials
F:\assets\Models
F:\assets\Models\Monkey
F:\assets\Models\Monkey\monkey.j3o
F:\assets\Scenes
F:\assets\Shaders
F:\assets\Sounds
F:\assets\Textures
F:\assets\Textures\Monkey
F:\assets\Textures\Monkey\DiffuseMap.png
</code></pre>
<p>在JME3中加载资源的代码如下：</p>
<pre><code>@Override
public void simpleInitApp() {
    // 配置文件目录
    assetManager.registerLocator(&quot;F:\\assets&quot;, FileLocator.class);
    
    // 加载j3o模型
    Spatial model = assetManager.loadModel(&quot;Models/Monkey/monkey.j3o&quot;);
    rootNode.attachChild(model);

    // 添加光源
    // ..
}
</code></pre>
<p>在实际开发中，不建议像这样使用<strong>绝对路径</strong>，而应该尽量使用<strong>相对路径</strong>来加载资产。</p>
<p>例如，可以在工程目录下创建一个 <code>res</code> 目录，用来存放游戏资产，然后通过 FileLocator 来管理它。</p>
<pre><code>@Override
public void simpleInitApp() {
    // 配置文件目录
    assetManager.registerLocator(&quot;res&quot;, FileLocator.class);
    
    // 加载j3o模型
    Spatial model = assetManager.loadModel(&quot;Models/Monkey/monkey.j3o&quot;);
    rootNode.attachChild(model);

    // 添加光源
    // ..
}
</code></pre>
<p>事实上，可以利用 FileLocator 来配置 <code>assets</code> 目录，这样在 Eclipse、IDEA等开发环境中就不需要把 <code>assets</code> 目录添加到 classpath 中了。</p>
<h4 id="ziplocator">ZipLocator</h4>
<p>ZipLocator 利用 Java 语言自带的 ZIP 算法实现了文件解压，JME3 可以通过 ZipLocator 直接从 zip 压缩文件中加载资产。</p>
<p>例如：把资产文件打包成 zip 文件，例如： <code>assets.zip</code> 文件结构如下：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/assets_zip.png" alt=""></p>
<p>把 <code>assets.zip</code> 放到 JME3 工程根目录下，然后在 AssetManager 中注册 <code>assets.zip</code> 文件所在的<strong>相对路径</strong>。</p>
<pre><code>@Override
public void simpleInitApp() {
    // 配置文件目录
    assetManager.registerLocator(&quot;assets.zip&quot;, ZipLocator.class);
    
    // 加载j3o模型
    Spatial model = assetManager.loadModel(&quot;Models/Monkey/monkey.j3o&quot;);
    rootNode.attachChild(model);

    // 添加光源
    // ..
}
</code></pre>
<p>当然，你也可以使用<strong>绝对路径</strong>，但我不建议你这么做。</p>
<pre><code>@Override
public void simpleInitApp() {
    // 配置文件目录
    assetManager.registerLocator(&quot;D:\\WORKSPACE\\jME3Projects\\MyGame\\assets.zip&quot;, ZipLocator.class);
    
    // 加载j3o模型
    Spatial model = assetManager.loadModel(&quot;Models/Monkey/monkey.j3o&quot;);
    rootNode.attachChild(model);

    // 添加光源
    // ..
}
</code></pre>
<h4 id="httpziplocator">HttpZipLocator</h4>
<p>HttpZipLocator 与 ZipLocator 的用法几乎完全相同，只不过它是通过 HTTP 协议去访问网上的资源包。</p>
<p><strong>假设</strong>我将 assets.zip 上传到网站上，提供静态资源路径 <code>http://www.jmecn.net/examples/assets.zip</code> 供用户下载，那么在 JME3 程序中就可以这样做：</p>
<pre><code>@Override
public void simpleInitApp() {
    // 配置文件目录
    assetManager.registerLocator(&quot;http://www.jmecn.net/examples/assets.zip&quot;,
            HttpZipLocator.class);
    
    // 加载模型
    Spatial model = assetManager.loadModel(&quot;Models/Monkey/monkey.j3o&quot;);
    rootNode.attachChild(model);

    // 添加光源
    // ..
}
</code></pre>
<h4 id="urllocator">UrlLocator</h4>
<p>UrlLocator 的用途与 FileLocator 相似，只不过它是通过 URL 来定位网络资产目录。</p>
<p><strong>假设</strong>我将 <code>F:\\assets</code> 目录中的内容上传到网站上，提供静态路径 <code>http://www.jmecn.net/examples/assets/</code> 供用户访问，那么在 JME3 程序中就可以这样做：</p>
<pre><code>@Override
public void simpleInitApp() {
    // 配置文件目录
    assetManager.registerLocator(&quot;http://www.jmecn.net/examples/assets/&quot;,
            UrlLocator.class);
    
    // 加载模型
    Spatial model = assetManager.loadModel(&quot;Models/Monkey/monkey.j3o&quot;);
    rootNode.attachChild(model);

    // 添加光源
    // ..
}
</code></pre>
<p>在这种方式下，用户应该可以通过超链接 <code>http://www.jmecn.net/examples/assets/Models/Monkey/monkey.j3o</code> 直接下载对应的模型文件。</p>
<h4 id="">多目录</h4>
<p>AssetManager 允许开发者注册多个资产目录，它会按照注册的顺序来加载资产，这个特性很有用。通过这种方式非常容易实现用户自定义皮肤等功能，还可以对不同版本的资产包进行排序。</p>
<p>在项目中定义两个资产根目录，<code>assets</code> 目录存放游戏本身的资产，<code>mod</code> 目录存放用户文件。先注册 <code>mod</code> 再注册 <code>assets</code>，AssetManager 就会先去加载用户文件；若文件不存在，才会去 <code>assets</code> 目录中加载文件。</p>
<pre><code>@Override
public void simpleInitApp() {
    // 配置文件目录
    assetManager.registerLocator(&quot;mod&quot;, FileLocator.class);
    assetManager.registerLocator(&quot;assets&quot;, FileLocator.class);
    
    // 加载j3o模型
    Spatial model = assetManager.loadModel(&quot;Models/Monkey/monkey.j3o&quot;);
    rootNode.attachChild(model);

    // 添加光源
    // ..
}
</code></pre>
<p>需要注意的是，JME3 在初始化时就已经执行了 <code>assetManager.registerLocator(&quot;/&quot;, ClasspathLocator.class)</code>。在上述规则下，它的顺位比所有 AssetLocator 都靠前。</p>
<p>如果你希望把其他资产目录排到 classpath 之前，先调用 <code>unregisterLocator</code> 方法取消注册 classpath，然后按自己期望的顺序添加 AssetLocator，再注册 classpath 即可。</p>
<p>代码如下：</p>
<pre><code>    // 取消注册 classpath
    assetManager.unregisterLocator(&quot;/&quot;, ClasspathLocator.class);

    // 注册mod资产目录
    assetManager.registerLocator(&quot;mod&quot;, FileLocator.class);
    
    // 重新注册 classpath
    assetManager.registerLocator(&quot;/&quot;, ClasspathLocator.class);
</code></pre>
]]></content:encoded></item><item><title><![CDATA[第十一章：加载音频文件]]></title><description><![CDATA[<p>jME3 支持单声道、双声道的音频文件；支持 wav 和 ogg 两种音频格式。加载 .ogg 格式的音频文件需要把 <code>jme3-jogg-{version}.jar</code> 添加到项目的依赖库。</p>
<p>jME3 将音频文件加载为 <code>com.jme3.audio.AudioNode</code>。下例演示了如何加载音频文件的两种方式：</p>
<p>缓存模式</p>
<pre><code>AudioNode gun = new AudioNode(assetManager, &quot;Sounds/Effects/Gun.wav&quot;, DataType.Buffer);
gun.setLooping(false);
gun.playInstance();
</code></pre>
<p>流模式</p>
<pre><code>AudioNode nature = new AudioNode(assetManager, &quot;Sound/Environment/</code></pre>]]></description><link>https://blog.jmecn.net/load-sounds/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b45</guid><category><![CDATA[资产管线]]></category><category><![CDATA[jMonkeyEngine]]></category><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Thu, 04 Jan 2018 06:37:52 GMT</pubDate><content:encoded><![CDATA[<p>jME3 支持单声道、双声道的音频文件；支持 wav 和 ogg 两种音频格式。加载 .ogg 格式的音频文件需要把 <code>jme3-jogg-{version}.jar</code> 添加到项目的依赖库。</p>
<p>jME3 将音频文件加载为 <code>com.jme3.audio.AudioNode</code>。下例演示了如何加载音频文件的两种方式：</p>
<p>缓存模式</p>
<pre><code>AudioNode gun = new AudioNode(assetManager, &quot;Sounds/Effects/Gun.wav&quot;, DataType.Buffer);
gun.setLooping(false);
gun.playInstance();
</code></pre>
<p>流模式</p>
<pre><code>AudioNode nature = new AudioNode(assetManager, &quot;Sound/Environment/Ocean Waves.ogg&quot;, DataType.Stream);
nature.setLooping(true);
nature.setPositional(true)
nature.setDirectional(false)
nature.play();
</code></pre>
<p>关于如何使用AudioNode，可参考：</p>
<ul>
<li><a href="https://jmonkeyengine.github.io/wiki/jme3/advanced/audio.html">Audio Node</a></li>
<li><a href="https://jmonkeyengine.github.io/wiki/jme3/advanced/audio_environment_presets.html">Audio Environment Presets</a></li>
<li><a href="http://blog.jmecn.net/chapter-11-3d-audio/">jME3初学者教程：3D音效</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[第十章：加载字体]]></title><description><![CDATA[<p>jME3 支持 BMFont字体，加载为 <code>com.jme3.font.BitmapFont</code> 对象。</p>
<p>Bitmap Font是AngleCode公司开发的一种技术，用于在3D场景中显示2D的文字。这种方法的原理是把要使用的文字做成一副图片，然后由程序根据文字的内容来动态选择图块，再绘制成一副新的图片。</p>
<h4 id="jme3">JME3内置字体</h4>
<p>jME3的内置一套BMFont字体，包含阿拉伯数字和大小写英文字母，用于显示系统的渲染状态。</p>
<p>下例演示了如何加载这套字体：</p>
<pre><code>BitmapFont font = assetManager.loadFont(&quot;Interface/Fonts/Default.fnt&quot;);
</code></pre>
<h4 id="">显示文字</h4>
<p>BitmapFont仅仅是字体，实际的文本内容用BitmapText表示。</p>
<pre><code>BitmapFont fnt = assetManager.loadFont(&quot;Interface/Fonts/Default.fnt&quot;);
BitmapText txt = new BitmapText(fnt, false);
txt.</code></pre>]]></description><link>https://blog.jmecn.net/load-fonts/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b44</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Thu, 04 Jan 2018 06:32:19 GMT</pubDate><content:encoded><![CDATA[<p>jME3 支持 BMFont字体，加载为 <code>com.jme3.font.BitmapFont</code> 对象。</p>
<p>Bitmap Font是AngleCode公司开发的一种技术，用于在3D场景中显示2D的文字。这种方法的原理是把要使用的文字做成一副图片，然后由程序根据文字的内容来动态选择图块，再绘制成一副新的图片。</p>
<h4 id="jme3">JME3内置字体</h4>
<p>jME3的内置一套BMFont字体，包含阿拉伯数字和大小写英文字母，用于显示系统的渲染状态。</p>
<p>下例演示了如何加载这套字体：</p>
<pre><code>BitmapFont font = assetManager.loadFont(&quot;Interface/Fonts/Default.fnt&quot;);
</code></pre>
<h4 id="">显示文字</h4>
<p>BitmapFont仅仅是字体，实际的文本内容用BitmapText表示。</p>
<pre><code>BitmapFont fnt = assetManager.loadFont(&quot;Interface/Fonts/Default.fnt&quot;);
BitmapText txt = new BitmapText(fnt, false);
txt.setSize(0.5f);
txt.setText(&quot;Hello world!&quot;);
rootNode.attachChild(txt);
</code></pre>
<p>效果：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/font_3d.png" alt=""></p>
<p><code>BitmapText</code> 是 <code>Node</code> 的子类，可以直接添加到 <code>rootNode</code> 中，显示为3D场景中的物体。若是希望将其显示为GUI，则应该添加到 <code>guiNode</code> 中。</p>
<pre><code>// 加载字体
BitmapFont fnt = assetManager.loadFont(&quot;Interface/Fonts/Default.fnt&quot;);
BitmapText txt = new BitmapText(fnt, false);
txt.setSize(32f);// &lt;--- GUI 单位是像素
txt.setText(&quot;Hello world!&quot;);

guiNode.attachChild(txt);

// 居中
float screenWidth = cam.getWidth();
float screenHeight = cam.getHeight();
float txtWidth = txt.getLineWidth();
float txtHeight = txt.getHeight();

float x = (screenWidth - txtWidth) * 0.5f;
float y = (screenHeight + txtHeight) * 0.5f;
txt.setLocalTranslation(x, y, 0);
</code></pre>
<p>效果如下：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/font_gui.png" alt=""></p>
<h4 id="">制作字体</h4>
<p>BMFont能显示的字符数量是有限的。如果你想在游戏中显示中文，就需要自己制作字体文件。如果想要显示不同的字体、字形，也得制作不同的字体文件。</p>
<p>详细用法请参考：</p>
<ul>
<li><a href="http://blog.jmecn.net/chapter-10-graphics-user-interface/#%E4%BD%BF%E7%94%A8BitmapFont">使用BitmapFont</a></li>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-examples/src/main/java/jme3test/gui/TestBitmapFont.java">jme3test.gui.TestBitmapFont</a></li>
<li><a href="https://github.com/jMonkeyEngine/jmonkeyengine/blob/master/jme3-examples/src/main/java/jme3test/gui/TestBitmapText3D.java">jme3test.gui.TestBitmapText3D</a></li>
</ul>
<h4 id="ttf">TTF字体</h4>
<p>若想在jME3中加载ttf字体，需要第三方插件支持，详见：</p>
<ul>
<li><a href="https://hub.jmonkeyengine.org/t/jme-truetypefont-rendering-library/35395">jME TrueTypeFont Rendering Library</a></li>
<li><a href="http://blog.jmecn.net/chapter-10-graphics-user-interface/#%E4%BD%BF%E7%94%A8TTF%E5%AD%97%E4%BD%93">使用TTF字体</a></li>
<li><a href="https://bintray.com/tryder/maven/jme-ttf">https://bintray.com/tryder/maven/jme-ttf</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[第九章：加载纹理]]></title><description><![CDATA[<p>jME3 支持下面4种纹理：</p>
<ul>
<li>Texture2D：最常用的纹理，其实就是一张2D位图；</li>
<li>Texture3D：一组 <code>Texture2D</code>对象，常用于体渲染；</li>
<li>TextureCubeMap：由前、后、左、右、上、下共6个Texture2D组成，通常用于表示天空盒；</li>
<li>TextureArray：OpenGL 3.0 以后的新技术，将一组Texture2D对象一次提交给GPU，可用于加速地形渲染等需要多个纹理的场景。</li>
</ul>
<p>这些纹理都可以使用 <code>com.jme3.texture.Texture</code> 对象表示。</p>
<p>如果你对“纹理”这个词不太了解，请查阅 <a href="https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/">Opengl中文教程：纹理</a>。</p>
<h3 id="">演示代码</h3>
<p>下例演示了如何加载纹理：</p>
<pre><code>Texture texture = assetManager.loadTexture(&quot;Textures/Monkey/DiffuseMap.png&quot;);
</code></pre>
<p>纹理加载后，</p>]]></description><link>https://blog.jmecn.net/load-textures/</link><guid isPermaLink="false">614c27b6fdc1cd59efaf7b43</guid><dc:creator><![CDATA[冰点]]></dc:creator><pubDate>Thu, 04 Jan 2018 06:04:41 GMT</pubDate><content:encoded><![CDATA[<p>jME3 支持下面4种纹理：</p>
<ul>
<li>Texture2D：最常用的纹理，其实就是一张2D位图；</li>
<li>Texture3D：一组 <code>Texture2D</code>对象，常用于体渲染；</li>
<li>TextureCubeMap：由前、后、左、右、上、下共6个Texture2D组成，通常用于表示天空盒；</li>
<li>TextureArray：OpenGL 3.0 以后的新技术，将一组Texture2D对象一次提交给GPU，可用于加速地形渲染等需要多个纹理的场景。</li>
</ul>
<p>这些纹理都可以使用 <code>com.jme3.texture.Texture</code> 对象表示。</p>
<p>如果你对“纹理”这个词不太了解，请查阅 <a href="https://learnopengl-cn.github.io/01%20Getting%20started/06%20Textures/">Opengl中文教程：纹理</a>。</p>
<h3 id="">演示代码</h3>
<p>下例演示了如何加载纹理：</p>
<pre><code>Texture texture = assetManager.loadTexture(&quot;Textures/Monkey/DiffuseMap.png&quot;);
</code></pre>
<p>纹理加载后，要作为材质参数设置给 Material 对象，并应用到 Geometry 上才能够显示。</p>
<pre><code>// 加载纹理
Texture texture = assetManager.loadTexture(&quot;Textures/Monkey/DiffuseMap.png&quot;);

// 设置为Unshaded材质的 ColorMap 参数
Material mat = new Material(assetManager, &quot;Common/MatDefs/Misc/Unshaded.j3md&quot;);
mat.setTexture(&quot;ColorMap&quot;, texture);

// 将材质应用到 Geometry 上
Geometry geom = new Geometry(&quot;Quad&quot;, new Quad(1, 1) );
geom.setMaterial(mat);
rootNode.attachChild(geom);
</code></pre>
<h3 id="texturekey">TextureKey</h3>
<p>除了使用路径直接加载纹理，还可以使用 TextureKey 和 loadAsset 方法来加载。</p>
<pre><code>TextureKey texKey = new TextureKey(&quot;Textures/Monkey/DiffuseMap.png&quot;);
Texture texture = assetManager.loadAsset(texKey);
</code></pre>
<p>TextureKey的作用很多，包括管理纹理缓存、翻转图像、生成MipMap等。</p>
<p><strong>缓存</strong></p>
<p>通过调用 <code>assetManager.deleteFromCache(texKey);</code> 方法，可以释放纹理缓存，等待GC回收。</p>
<pre><code>assetManager.deleteFromCache(texKey);
</code></pre>
<h4 id="">翻转纹理</h4>
<p>为了兼容D3D和OpenGL的纹理坐标系，可能需要把纹理上下翻转。jME3加载纹理时，默认是上下翻转的。</p>
<p>你可以在 Texture 类的构造方法中传入一个 boolean 变量控制翻转方式。</p>
<pre><code>// 不翻转
TextureKey texKey = new TextureKey(&quot;Textures/Examples/yan.png&quot;, false);
Texture texture = assetManager.loadAsset(texKey);
</code></pre>
<p>也可以直接调用 <code>setFlipY(boolean flipY)</code> 方法来控制翻转方式。</p>
<pre><code>TextureKey texKey = new TextureKey(&quot;Textures/Examples/yan.png&quot;);
texKey.setFlipY(false);// 不翻转
Texture texture = assetManager.loadAsset(texKey);
</code></pre>
<p><code>setFlipY(true)</code> ：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/yan.png" alt=""></p>
<p><code>setFlipY(false)</code> ：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/yan_flipY.png" alt=""></p>
<h4 id="">多级纹理映射</h4>
<p>jME3可以为纹理自动生成MipMap，这将提高渲染远处物体时的效率和质量，但会额外增加1/3的纹理内存开销。</p>
<p>开启这个功能，需要在加载纹理之前调用 TextureKey 的 <code>setGenerateMips(true)</code> 方法。</p>
<pre><code>TextureKey texKey = new TextureKey(&quot;Textures/Examples/yan.png&quot;);
texKey.setGenerateMips(true);// 生成MipMap
Texture texture = assetManager.loadAsset(texKey);
</code></pre>
<h4 id="">各项异性滤波</h4>
<p>一般端游都会把各向异性滤波（Anisotropic Filtering）参数设置为 4/8/16，这会提高从不同角度渲染远处物体的质量，但FPS可能降低10~40帧左右。jME3 默认不开启各项异性滤波，即 <code>anisotropy = 0</code>。</p>
<p>开启这个功能，需要在加载纹理之前调用 TextureKey 的 <code>setAnisotropy(int anisotropy)</code> 方法。</p>
<pre><code>TextureKey texKey = new TextureKey(&quot;Textures/Examples/yan.png&quot;);
texKey.setAnisotropy(4);// 开启各项异性
Texture texture = assetManager.loadAsset(texKey);
</code></pre>
<h3 id="">纹理环绕方式</h3>
<p>纹理坐标的范围通常是从(0, 0)到(1, 1)，那如果纹理坐标的值在这个范围之外会发生什么？OpenGL默认的行为是重复这个纹理图像（我们基本上忽略浮点纹理坐标的整数部分），但OpenGL提供了更多的选择：</p>
<table>
    <tr><th>jME3</th><th>OpenGL</th><th>描述</th></tr>
    <tr>
        <td>WrapMode.Repeat</td>
        <td>GL_REPEAT</td>
        <td>对纹理的默认行为。重复纹理图像。</td>
    </tr>
    <tr>
        <td>WrapMode.MirroredRepeat</td>
        <td>GL_MIRRORED_REPEAT</td>
        <td>和Repeat一样，但每次重复图片是镜像放置的。</td>
    </tr>
    <tr>
        <td>WrapMode.EdgeClamp</td>
        <td>GL_CLAMP_TO_EDGE</td>
        <td>纹理坐标会被约束在0到1之间，超出的部分会重复纹理坐标的边缘，产生一种边缘被拉伸的效果。</td>
    </tr>
</table>
<p><em>注：还有很多OpenGL 3.0版本以前的围绕方式，新的显卡已经不建议使用，故没有全部列出。</em></p>
<p>在加载纹理后，通过调用 Texture 的 <code>setWrap(WrapMode mode)</code> 方法可以设置纹理包围模式。</p>
<pre><code>Texture texture = assetManager.loadTexture(&quot;Textures/Examples/yan.png&quot;);
texture.setWrap(WrapMode.Repeat);
</code></pre>
<p>下图是分别设置为 <code>Repeat</code>、<code>MirroredRepeat</code>、<code>EdgeClamp</code> 模式后的纹理。</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/wrap_mode.png" alt=""></p>
<p>由于纹理坐标系有S、T两个坐标轴，因此可以 调用 <code>setWrap(WrapAxis axis, WrapMode mode)</code> 方法来为纹理分别设置不同方向的纹理包围方式。</p>
<pre><code>Texture texture = assetManager.loadTexture(&quot;Textures/Examples/yan.png&quot;);
texture.setWrap(WrapAxis.S, WrapMode.Repeat);
texture.setWrap(WrapAxis.T, WarpMode.MirroredRepeat);
</code></pre>
<p>横(S)轴采用复制模式，竖(T)轴采用镜像复制模式，效果如下：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/wrap_mode_st.png" alt=""></p>
<p>对于 Texture3D 来说，除了ST坐标轴外，还有“深度”坐标轴，可以通过 <code>WarpAxis.R</code> 来指定。</p>
<pre><code>Texture3D texture = (Texture3D)assetManager.loadTexture(&quot;Textures/Noise/Noise3D.dds&quot;);
texture.setWrap(WrapAxis.S, WrapMode.Repeat);
texture.setWrap(WrapAxis.T, WrapMode.Repeat);
texture.setWrap(WrapAxis.R, WrapMode.Repeat);
</code></pre>
<h3 id="">纹理滤波</h3>
<h4 id="">放大滤波</h4>
<p>当物体很大但纹理分辨率很低时，将启用放大滤波（MagFilter）。jME3 支持 OpenGL 的最邻近滤波(<code>GL_NEAREST</code>)和二次线性滤波(<code>GL_LINEAR</code>)，前者会渲染出像素风格的画面，而后者会显得比较模糊。</p>
<table>
    <tr><th>jME3</th><th>OpenGL</th><th>描述</th></tr>
    <tr>
        <td>MagFilter.Nearest</td>
        <td>GL_NEAREST</td>
        <td>选择中心点最接近纹理坐标的那个像素，看起来会有像素风格。</td>
    </tr>
    <tr>
        <td>MagFilter.Bilinear</td>
        <td>GL_LINEAR</td>
        <td>基于纹理坐标附近的纹理像素，计算出一个插值，近似出这些纹理像素之间的颜色。</td>
    </tr>
</table>
<p>通过调用 Texture 的 <code>setMagFilter(MagFilter filter)</code> 方法可以设置放大滤波方式，代码如下。</p>
<pre><code>Texture texture = assetManager.loadTexture(&quot;Textures/Examples/yan.png&quot;);
texture.setMagFilter(MagFilter.Nearest);// 采用最邻近滤波
</code></pre>
<p>下图是分别使用 <code>Nearest</code> 和 <code>Bilinear</code> 滤波的效果：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/mag_filter.png" alt=""></p>
<h4 id="">缩小滤波</h4>
<p>若物体在画面上看起来只有几个像素大小，使用 2K 分辨率的纹理是毫无意义的。当物体很小但纹理分辨率很高时，将启用缩小滤波（MinFilter）。缩小滤波通常和多级渐远纹理(<code>MipMap</code>) 一起使用。</p>
<p>jME3 支持 OpenGL 的最邻近滤波和线性滤波，这两种滤波方式和 MipMap 又额外搭配出 4 种组合。</p>
<table>
    <tr><th>jME3</th><th>OpenGL</th><th>描述</th></tr>
    <tr>
        <td>MinFilter.NearestNoMipMaps</td>
        <td>GL_NEAREST</td>
        <td>最近邻滤波</td>
    </tr>
    <tr>
        <td>MinFilter.BilinearNoMipMaps</td>
        <td>GL_LINEAR</td>
        <td>二次线性滤波</td>
    </tr>
    <tr>
        <td>MinFilter.NearestNearestMipMap</td>
        <td>GL_NEAREST_MIPMAP_NEAREST</td>
        <td>使用最邻近MIPMAP的最近邻滤波</td>
    </tr>
    <tr>
        <td>MinFilter.BilinearNearestMipMap</td>
        <td>GL_LINEAR_MIPMAP_NEAREST</td>
        <td>使用最邻近MIPMAP的二次线性滤波</td>
    </tr>
    <tr>
        <td>MinFilter.NearestLinearMipMap</td>
        <td>GL_NEAREST_MIPMAP_LINEAR</td>
        <td>使用MIPMAP级别之间线性插值的最近邻滤波</td>
    </tr>
    <tr>
        <td>MinFilter.Trilinear</td>
        <td>GL_LINEAR_MIPMAP_LINEAR</td>
        <td>三次线性滤波（使用MIPMAP级别之间线性插值的二次线性滤波）</td>
    </tr>
</table>
<p>注意：如果使用后4种滤波方式，应该先用 TextureKey 设置生成 MipMap。</p>
<p>通过调用 Texture 的 <code>setMinFilter(MinFilter filter)</code> 方法可以设置缩小滤波方式，代码如下。</p>
<pre><code>TextureKey texKey = new TextureKey(&quot;Textures/Examples/yan.png&quot;);
texKey.setGenerateMips(true);
Texture texture = assetManager.loadTexture(texKey);
texture.setMinFilter(MinFilter.BilinearNearestMipMap);// 采用最邻近MIPMAP的二次线性滤波
</code></pre>
<p>下图是分别关闭和开启 <code>BilinearNearestMipMap</code> 滤波的效果：</p>
<p><img src="https://blog.jmecn.net/content/images/2018/01/min_filter.png" alt=""></p>
]]></content:encoded></item></channel></rss>