我想将包含位图字体字形的纹理直接作为libGDX中的Image渲染到屏幕上。当使用程序(例如Hiero)制作位图字体时,它将生成文本可读的.fnt文件以及.png文件,该文件是该字体的精灵表。唯一缺少的是匹配的.atlas文件,以告诉该.png中纹理的位置。

该程序将一个.fnt文件作为输入,并输出一个可以与libGDX(以及使用相同类型的Atlas文件的任何引擎)一起使用的.atlas文件。它解析字体文件,以查找纹理名称及其在Sprite表上的位置。

我寻求反馈的一个原因是,这是我在Github上放置的第一个程序/代码,其他人使用它的意图。听到是否有足够的注释和足够的文档供其他人理解和使用软件会很有趣。

Launcher.java

public class Launcher {

    /**
     * The file name for the atlas generator must be passed in
     * without a file extension.
     */
    public static void main(String[] args) throws IOException {
        String fileName = "test_dos437";
        new FntToAtlasGenerator(fileName);
    }
}


FntToAtlasGenerator.java

/**
 * The idea is to pass in the name of a .fnt file generated by Hiero
 * This program will generate a .atlas file that is compatible with libGDX
 * Next put the .atlas file and the .png that comes along with the .fnt file
 * into the android/assets folder of your libGDX project.
 * 
 * @author baz
 *
 */
public class FntToAtlasGenerator {

    List<GlyphData> glyphs = new ArrayList<GlyphData>();

    public FntToAtlasGenerator(String fileName) throws IOException {

        //String fileName = "test_dos437";
        String inputDir = "input/";
        String outputDir = "output/";
        String extension = ".fnt";
        String atlasExtension = ".atlas";

        FileReader fontReader = new FileReader(inputDir + fileName + extension);
        BufferedReader reader = new BufferedReader(fontReader);

        reader.readLine(); //info line
        String commonLine = reader.readLine();
        String pageLine = reader.readLine();
        reader.readLine(); //chars line

        String line = reader.readLine();
        while(line != null) {
            this.addLineToGlyphs(line);
            line = reader.readLine();
        }

        reader.close();

        PrintWriter writer = new PrintWriter(outputDir + fileName + atlasExtension, "UTF-8");

        //values read from .fnt file
        String fileNameForAtlas = this.getFileNameForPageLine(pageLine);
        String size = this.getSizeForCommonLine(commonLine);

        //default values
        String format = "RGBA8888";
        String filter = "Nearest, Nearest";
        String repeat = "none";

        this.writeOpeningLines(writer, fileNameForAtlas, size, format, filter, repeat);

        for (GlyphData glyph : this.glyphs) {
            this.writeGlyph(glyph, writer);
        }

        writer.close();
    }

    private void writeOpeningLines(PrintWriter writer, String fileName, String size, String format, String filter, String repeat) {
        writer.println(fileName);
        writer.println("size: " + size);
        writer.println("format: " + format);
        writer.println("filter: " + filter);
        writer.println("repeat: " + repeat);
    }

    /**
     * The name will be a string that is the integer of the character in ASCII
     * The idea is that you can get the integer value of a character in a string
     * and then render its image to the screen
     */
    private void writeGlyph(GlyphData glyph, PrintWriter writer) {
        String stringOffset = "  "; //two spaces for lines after name
        writer.println(glyph.id); //name
        writer.println(stringOffset + "rotate: false");
        writer.println(stringOffset + "xy: " + glyph.x + ", " + glyph.y);
        writer.println(stringOffset + "size: " + glyph.width + ", " + glyph.height);
        writer.println(stringOffset + "orig: " + glyph.width + ", " + glyph.height);
        writer.println(stringOffset + "offset: " + glyph.xoffset + ", " + glyph.yoffset);
        writer.println(stringOffset + "index: -1");
    }

    private String getFileNameForPageLine(String pageLine) {
        String[] fragments = pageLine.split(" ");
        String nameString = fragments[2];
        return nameString.replace("file=", "").replace("\"", "");
    }

    private String getSizeForCommonLine(String commonLine) {
        String[] fragments = commonLine.split(" ");
        String widthString = fragments[3];
        widthString = widthString.replace("scaleW=", "");
        String heightString = fragments[4];
        heightString = heightString.replace("scaleH=", "");
        return widthString + "," + heightString;
    }

    private void addLineToGlyphs(String lineString) throws IOException {
        if (lineString != null) {
            String[] lineFragments = lineString.split(" ");
            List<String> formattedStrings = new ArrayList<String>();

            //remove new line, space, and return characters
            //because there are wacky spaces in between the text of the .fnt file
            //and when you split on space, it adds new line type characters
            if (lineFragments[0].equals("char")) {
                for (int i = 0; i < lineFragments.length; i++) {
                    String string = lineFragments[i];
                    string = string.replace(" ", "");
                    string = string.replace("\n", "");
                    string = string.replace("\r", "");
                    //cant just reassign, because we need to remove empties
                    //and we want to directly assign based on index because we know the format
                    if (!(string.equals(" ") ||
                          string.equals("\n") ||
                          string.equals("\r") ||
                          string.isEmpty())) {
                        formattedStrings.add(string);
                    }
                }

                /*
                for (String string : formattedStrings) {
                    System.out.println(string);
                }
                */

                GlyphData data = new GlyphData(formattedStrings);
                this.glyphs.add(data);
            }
        }
    }
}

//example input
/*
info face="Pescadero" size=20 bold=1 italic=0 charset="" unicode=0 stretchH=100 smooth=1 aa=1 padding=2,2,2,2 spacing=2,2
        common lineHeight=31 base=19 scaleW=256 scaleH=256 pages=1 packed=0
        page id=0 file="pescadero-blackWhite-20.png"
        chars count=94
char id=32   x=0     y=0     width=0     height=0     xoffset=0     yoffset=19    xadvance=13     page=0  chnl=0 
        char id=124   x=0     y=0     width=7     height=26     xoffset=2     yoffset=2    xadvance=17     page=0  chnl=0 
        char id=92   x=7     y=0     width=14     height=25     xoffset=-2     yoffset=2    xadvance=14     page=0  chnl=0 
        char id=47   x=21     y=0     width=14     height=25     xoffset=-2     yoffset=2    xadvance=14     page=0  chnl=0 
        char id=106   x=35     y=0     width=10     height=24     xoffset=-2     yoffset=4    xadvance=12     page=0  chnl=0 
        char id=81   x=45     y=0     width=22     height=23     xoffset=-1     yoffset=4    xadvance=23     page=0  chnl=0 
        char id=74   x=67     y=0     width=12     height=23     xoffset=-2     yoffset=3    xadvance=13     page=0  chnl=0 
        char id=93   x=79     y=0     width=10     height=22     xoffset=-2     yoffset=3    xadvance=13     page=0  chnl=0 
        char id=91   x=89     y=0     width=10     height=22     xoffset=0     yoffset=3    xadvance=13     page=0  chnl=0 
        char id=41   x=99     y=0     width=11     height=22     xoffset=-2     yoffset=4    xadvance=13     page=0  chnl=0 
        char id=40   x=110     y=0     width=11     height=22     xoffset=-1     yoffset=4    xadvance=13     page=0  chnl=0 
        char id=112   x=121     y=0     width=16     height=22     xoffset=-2     yoffset=7    xadvance=18     page=0  chnl=0 
        kearnings count = -1
*/
//example output
/*
texturePackResize22.png
size: 1784,1498
format: RGBA8888
filter: Nearest,Nearest
repeat: none
arco01
  rotate: false
  xy: 164, 326
  size: 160, 318
  orig: 160, 318
  offset: 0, 0
  index: -1
arco02
  rotate: false
  xy: 326, 752
  size: 160, 318
  orig: 160, 318
  offset: 0, 0
  index: -1
arco03
  rotate: false
  xy: 488, 1178
  size: 160, 318
  orig: 160, 318
  offset: 0, 0
  index: -1
*/


GlyphData.java

public class GlyphData {

    public final String character;
    public final String id;
    public final String x;
    public final String y;
    public final String width;
    public final String height;
    public final String xoffset;
    public final String yoffset;
    public final String xadvance;
    public final String page;
    public final String chnl;

    public GlyphData(List<String> glyphDataFragments) {

        //preserving all non white space elements of the char line of the .fnt file
        //some of the data may be needed later
        //im leaving this block in so it is clear which are currently unused
        String character = glyphDataFragments.get(0); //keyword for font language
        String id = glyphDataFragments.get(1);
        String x = glyphDataFragments.get(2);
        String y = glyphDataFragments.get(3);
        String width = glyphDataFragments.get(4);
        String height = glyphDataFragments.get(5);
        String xoffset = glyphDataFragments.get(6);
        String yoffset = glyphDataFragments.get(7);
        String xadvance = glyphDataFragments.get(8);
        String page = glyphDataFragments.get(9);
        String chnl = glyphDataFragments.get(10);

        this.id = id.replace("id=", "");
        this.x = x.replace("x=", "");
        this.y = y.replace("y=", "");
        this.width = width.replace("width=", "");
        this.height = height.replace("height=", "");
        this.xoffset = xoffset.replace("xoffset=", "");
        this.yoffset = yoffset.replace("yoffset=", "");

        //unused
        this.character = character;
        this.xadvance = xadvance;
        this.page = page;
        this.chnl = chnl;
    }
}


放置生成的.atlas文件和.png文件时在libGDX项目的android/assets文件夹中,并从atlas文件创建TextureAtlas对象,您可以根据字符的ASCII整数值访问TextureRegion。例如,70等于大写F。然后,您可以从Image创建一个TextureRegion对象,并像使用其他任何精灵一样使用它。

这是libGDX中的用法:

Map<Integer, TextureRegion> textures =  new HashMap<Integer, TextureRegion>();
TextureAtlas atlas = new TextureAtlas("bitmapfont.atlas");

final Image charImage = new Image(this.libGDXGame.allTextures.get((int)character));


并对可能的情况进行了漂亮的描述:没有依赖关系,自述文件说明了如何使用该程序。这是链接。

评论

这与TextureRegion charImage =(new com.badlogic.gdx.graphics.g2d.BitmapFont(Gdx.files.internal(“ data / myfile.bmf”)。getRegion(character))有何不同?

可能不是,我将不得不尝试一下,看看结果是否相同。

#1 楼

通过请求将我的评论变成答案。您只需使用LibGDX中已内置的功能就可以执行所需的操作。

注意:我没有尝试过,我只知道该类存在并且应该可以提取该类。您想要的数据,因此:您的零件可能需要一些组装。

LibGDX具有加载和处理位图字体的功能。请参阅此处的文档。

您可以使用BitmapFont类加载字体数据。它支持AngelCode BMFont格式。您正在使用的Hiero可以输出到BMFont。

创建BitmapFont的新实例:

BitmapFont bmf = new BitmapFont(Gdx.files.internal("data/myfile.bmf"));


然后获取支持字体的数据:

BitmapFont.BitmapFontData bmfdata = bmf.getData();


获取所需字符的Glyph,其中包含字形的u / v坐标,其位于哪个纹理页面以及一些其他优点。然后从BitmapFont获取正确的纹理页面,并使用u / v对为所需的字形提取纹理区域:

BitmapFont.Glyph glyph = bmfdata.getGlyph(character);

if(glyph == null){
    // handle error: No glyph for character
}
else{
    TextureRegion page = bmf.getRegion(glyph.page);
    TextureRegion glyphTexture = new TextureRegion(page.getTexture(), glyph.u, glyph.v, glyph.u2, glyph.v2);
    // Use glyphTexture to render, or store it somewhere. 
}


再次,我没有测试过。您可能不得不摆弄或使用字形的其他属性来获得所需的结果。但是您所需的数据就在其中,您只需撬开它即可。 LibGDX文档(和源代码)是您的朋友。

评论


\ $ \ begingroup \ $
我确认这确实有效!我感谢您向我展示了一个新的libGDX技巧:)
\ $ \ endgroup \ $
–巴佐拉
15年8月25日在20:52

#2 楼

构造函数的用途

构造函数的用途是创建对象。
FntToAtlasGenerator的构造函数完全是另外一回事:
它需要一些输入并在文件系统中生成一些输出,并且该类没有其他公共方法和用途。
因此,glyphs成员变量是没有意义的,
,并且您在此构造函数中拥有的所有代码实际上应该都在实用程序方法中。 >
单责任原则

将构造方法转换为实用方法是不够的。
例程应该承担单一责任,
构造方法中的代码具有更多:


读取输入数据
写输出数据

这些应该分开。

使用增强型for-each loop

addLineToGlyphs中的for循环计数不需要循环索引。
因此应将其重写为增强的for-each循环:

for (String lineFragment : lineFragments) {
    // ...
}


在最小范围内声明变量ary

addLineToGlyphs中,
可以在List<String> formattedStrings条件内移动if (lineFragments[0].equals("char")) {的声明,
因为不需要它。

命名

addLineToGlyphs中,
建立一个名为formattedStrings的列表,
并将其传递给GlyphData的构造函数,
其中参数名为glyphDataFragments
一个更好的名称,
也可以在addLineToGlyphs中采用它。

无意义的语句

在此代码中:
    string = string.replace(" ", "");
    string = string.replace("\n", "");
    string = string.replace("\r", "");
    //cant just reassign, because we need to remove empties
    //and we want to directly assign based on index because we know the format
    if (!(string.equals(" ") ||
          string.equals("\n") ||
          string.equals("\r") ||
          string.isEmpty())) {



相等条件都是毫无意义的,
感谢之前的替换。
您可以将if语句简化为:

    if (!string.isEmpty()) {


构建路径

代替此:


    FileReader fontReader = new FileReader(inputDir + fileName + extension);



从父目录和文件名构造路径的更简洁方法:

    FileReader fontReader = new FileReader(new File(inputDir, fileName + extension));


格式化字符串输出

像这样的复杂字符串连接可能很难阅读,
并且烦人键入,打断带引号的字符串以插入变量:


  writer.println(stringOffset + "xy: " + glyph.x + ", " + glyph.y);



这是您可能考虑的同一事物的替代写作风格:

    writer.printf(stringOffset + "xy: %s, %s%n", glyph.x, glyph.y);


#3 楼

由于您有可用的Java 7,因此您绝对应该对那里的整个基于文件的代码进行重新处理。

您要开始做的第一件事就是使用java.nio.Path,与某些工具相比,它的响应速度更快,更干净与文件有关的奇怪事情。

您肯定应该做的下一件事是使用try-with-resources。然后,此外,您不必再在右侧指定通用签名,请使用菱形运算符。

更改为使用Path后,还应该适应“新方式”事情并使用java.io.Files提供的便捷方法使基于文件的流的构建变得更加容易。

到目前为止,这使我们能够像下面这样对FntToAtlasGenerator进行整理:

    List<GlyphData> glyphs = new ArrayList<>();

    public FntToAtlasGenerator(String fileName) throws IOException {

        String inputDir = "input/";
        String outputDir = "output/";
        String extension = ".fnt";
        String atlasExtension = ".atlas";

        String commonLine;
        String pageLine;
        try (BufferedReader reader = Files.newBufferedReader(Paths.get(inputDir, fileName + extension), Charset.forName("UTF-8"))) {
            reader.readLine(); //info line
            commonLine = reader.readLine();
            pageLine = reader.readLine();
            reader.readLine(); //chars line

            String line = reader.readLine();
            while (line != null) {
                this.addLineToGlyphs(line);
                line = reader.readLine();
            }
        }


        try (PrintWriter writer = new PrintWriter(
                Files.newOutputStream(Paths.get(outputDir, fileName + atlasExtension), StandardOpenOption.WRITE))
        ) {

            //values read from .fnt file
            String fileNameForAtlas = this.getFileNameForPageLine(pageLine);
            String size = this.getSizeForCommonLine(commonLine);

            //default values
            String format = "RGBA8888";
            String filter = "Nearest, Nearest";
            String repeat = "none";

            this.writeOpeningLines(writer, fileNameForAtlas, size, format, filter, repeat);

            for (GlyphData glyph : this.glyphs) {
                this.writeGlyph(glyph, writer);
            }
        }
    }


然后,Janos在他的评论中再次完全正确,他说构造函数应该负责创建对象。基本上,您的构造函数是一个很大的副作用。将FntToAtlasGenerator完全重构到静态上下文并不难,在该上下文中它可以停止假装为类

最后一件事……我上次检查时可以替换这些用更具表现力的.replaceAll重复执行replace语句:

fragment = fragment.replaceAll("[ \r\n]+", "");


(顺便说一句,这是在将string重命名为fragment之后)。同样,您不应该注释掉不再使用的代码。删除它,版本历史得到支持:)

#4 楼

建议


更改启动器代码以使用args设置输入和输出文件
重命名Launcher,使其从调用者的角度来说更有意义。可能是Fnt2Atlas吗?
将文件读取和写入移动到单独的部分,这样转换代码就可以在没有特定文件系统的情况下使用
考虑阅读有效的Java来改善通用代码样式


评论


\ $ \ begingroup \ $
喜欢评论这种“通用代码样式”吗? OP有什么问题?向我们推荐一本书就等于放弃了联系并走上了舞台。
\ $ \ endgroup \ $
–RubberDuck
15年8月23日在16:13

\ $ \ begingroup \ $
尽管有效的Java确实有大量有用的建议,但是您的评论太笼统而没用。使最后一点更具体,或者将其删除。另请注意,评论旁边的高票数要求提供更多详细信息,该社区正在表现出兴趣,请不要忽略它,您可能会得到一些赞成!
\ $ \ endgroup \ $
– janos
2015年9月5日在18:01