ugui位图字体使用 - fnt文件解析器

 用于解析BMFont软件生成的fnt文件

 

using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using UnityEngine;

public class FntParse
{
    public struct Kerning
    {
        public int first;
        public int second;
        public int amount;
    }

    private int m_ImageWidth;
    private int m_ImageHeight;
    private string[] m_ImageNames;

    private string m_FontName;
    private int m_FontSize;
    private int m_LineHeight;
    private int m_LineBaseHeight;

    private CharacterInfo[] m_CharInfos;
    private Kerning[] m_Kernings;

    public int imageWidth { get { return m_ImageWidth; } }
    public int imageHeight { get { return m_ImageHeight; } }
    public int imageCount { get { return m_ImageNames.Length; } }
    public string GetImageName(int index) { return m_ImageNames[index]; }

    public string fontName { get { return m_FontName; } }
    public int fontSize { get { return m_FontSize; } }
    public int lineHeight { get { return m_LineHeight; } }
    public int lineBaseHeight { get { return m_LineBaseHeight; } }

    public CharacterInfo[] charInfos { get { return m_CharInfos; } }

    public Kerning[] kernings { get { return m_Kernings; } }

    public static FntParse Parse(string text)
    {
        FntParse parse = null;
        if (text.StartsWith("info"))
        {
            parse = new FntParse();
            parse.ParseText(text);
        }
        return parse;
    }

    private Regex m_KVRegexPattern;
    private bool m_PrintLine = true;
    private int m_LineNum;

    public void ParseText(string content)
    {
        m_KVRegexPattern = new Regex(@"(\S+)=""?([\w-.]+)""?");
        var lines = content.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
        m_LineNum = 0;

        //第1行
        ParseInfoLine(lines[m_LineNum++]);
        //第2行
        ParseCommonLine(lines[m_LineNum++]);

        //字体图片多张的话就有多行
        for (int i = 0; i < m_ImageNames.Length; i++)
        {
            ParsePageLine(lines[m_LineNum++]);
        }

        // don't use count of chars, count is incorrect if has space 
        //ParseCharsOrKerningCount(ref lines[3]);
        m_LineNum++; //chars行跳过

        List<CharacterInfo> list = new List<CharacterInfo>();
        //chars count之后是char
        int maxLineNum = lines.Length;
        while (m_LineNum < maxLineNum)
        {
            if (ParseCharLine(lines[m_LineNum], ref list)) m_LineNum++;
            else break;
        }
        m_CharInfos = list.ToArray();

        // skip empty line
        while (m_LineNum < maxLineNum)
        {
            if (string.IsNullOrEmpty(lines[m_LineNum])) m_LineNum++;
            else break;
        }

        // kernings
        if (m_LineNum < maxLineNum)
        {
            int count = ParseCharsOrKerningCount(lines[m_LineNum++]);
            if (count > 0)
            {
                m_Kernings = new Kerning[count];
                for (var i = 0; i < count; i++)
                {
                    if (ParseKerningLine(lines[m_LineNum], ref m_Kernings[i])) m_LineNum++;
                    else break;
                }
            };
        }
    }

    public void PrintInfo()
    {
        Debug.Log($"font:{m_FontName}, fontSize:{m_FontSize}, lineHeight:{m_LineHeight}, lineBase:{m_LineBaseHeight}");
        Debug.Log($"image size:{m_ImageWidth}, {m_ImageHeight}");

        for (var i = 0; i < m_ImageNames.Length; ++i)
        {
            Debug.Log($"{i}, image name: {m_ImageNames[i]}");
        }

        Debug.Log($"chars: {m_CharInfos.Length}");
        for (var i = 0; i < m_CharInfos.Length; ++i)
        {
            var charInfo = m_CharInfos[i];
            Debug.Log($"char: {charInfo.index}");
        }

        if (null != m_Kernings)
        {
            Debug.Log($"kernings: {m_Kernings.Length}");
            for (var i = 0; i < m_Kernings.Length; ++i)
            {
                var ker = m_Kernings[i];
                Debug.Log($"kerning: {ker.first}, {ker.second}, {ker.amount}");
            }
        }
    }

    private void ParseInfoLine(string infoLine)
    {
        MatchCollection result = m_KVRegexPattern.Matches(infoLine);
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                //Debug.Log($"info: key:'{key}', value:'{value}'");
                switch (key)
                {
                    case "face": m_FontName = value; break;
                    case "size": m_FontSize = int.Parse(value); break;
                }
            }
            else
            {
                /*
                Debug.Log($"groups: {item.Groups.Count}");
                for (var j = 0; j < item.Groups.Count; ++j)
                {
                    Group g = item.Groups[j];
                    Debug.Log($"{j}: v:{g.Value}, index:{g.Index}, len:{g.Length}, succ:{g.Success}");
                }
                */
            }
        }
    }

    private void ParseCommonLine(string commonLine)
    {
        MatchCollection result = m_KVRegexPattern.Matches(commonLine);
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                switch (key)
                {
                    case "lineHeight": m_LineHeight = int.Parse(value); break; //行top与行bottom间的距离为行高
                    case "base": m_LineBaseHeight = int.Parse(value); break; //行top往下ascent的距离就是baseline(基线)的位置
                    case "scaleW": m_ImageWidth = int.Parse(value); break;
                    case "scaleH": m_ImageHeight = int.Parse(value); break;
                    case "pages":
                        var num = int.Parse(value);
                        m_ImageNames = new string[num];
                        if (num > 1)
                            Debug.LogWarning($"more than 1 font Images, only support 1 Image");
                        break;
                }
            }
        }
    }

    private void ParsePageLine(string pageLine)
    {
        int pageId = -1;
        string fileName = "";

        MatchCollection result = m_KVRegexPattern.Matches(pageLine);
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                //Debug.Log($"page: key='{key}', value='{value}'");
                switch (key)
                {
                    case "file": fileName = value; break;
                    case "id": pageId = int.Parse(value); break;
                }
            }
        }

        if (-1 != pageId)
            m_ImageNames[pageId] = fileName;
    }

    private int ParseCharsOrKerningCount(string line)
    {
        MatchCollection result = m_KVRegexPattern.Matches(line);
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                switch (key)
                {
                    case "count":
                        return int.Parse(value);
                }
            }
        }
        return 0;
    }

    private bool ParseCharLine(string charLine, ref List<CharacterInfo> list)
    {
        //if (m_PrintLine) Debug.Log($"{m_LineNum}: {charLine}");

        if (!charLine.StartsWith("char ")) return false;

        int id = 0, x = 0, y = 0, width = 0, height = 0;
        int xoffset = 0, yoffset = 0, xadvance = 0;
        int page = 0;

        MatchCollection result = m_KVRegexPattern.Matches(charLine);
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                //Debug.Log($"char: key:'{key}', value:'{value}'");
                switch (key)
                {
                    case "id": id = int.Parse(value); break;
                    case "x": x = int.Parse(value); break;
                    case "y": y = int.Parse(value); break;
                    case "width": width = int.Parse(value); break;
                    case "height": height = int.Parse(value); break;
                    case "xoffset": xoffset = int.Parse(value); break;
                    case "yoffset": yoffset = int.Parse(value); break;
                    case "xadvance": xadvance = int.Parse(value); break;
                    case "page": page = int.Parse(value); break;
                }
            }
        }

        list.Add(CreateCharInfo(id, x, y, width, height, xoffset, yoffset, xadvance, page));
        return true;
    }

    private bool ParseKerningLine(string kerningLine, ref Kerning kerning)
    {
        if (!kerningLine.StartsWith("kerning")) return false;

        MatchCollection result = m_KVRegexPattern.Matches(kerningLine);
        for (var i = 0; i < result.Count; ++i)
        {
            Match item = result[i];
            if (3 == item.Groups.Count)
            {
                var key = item.Groups[1].Value;
                var value = item.Groups[2].Value;
                //Debug.Log($"kerning: key:'{key}', value:'{value}'");
                switch (key)
                {
                    case "first": kerning.first = int.Parse(value); break;
                    case "second": kerning.second = int.Parse(value); break;
                    case "amount": kerning.amount = int.Parse(value); break;
                }
            }
        }

        return true;
    }

    /// <summary>
    /// 从fnt中的char行, 创建一个CharacterInfo对象
    /// </summary>
    /// <param name="id">ascii码或unicode码</param>
    /// <param name="x">字体图片中的x坐标, 左上角为(0, 0)</param>
    /// <param name="y">字体图片中的y坐标, 左上角为(0, 0)</param>
    /// <param name="width">字符图片宽度</param>
    /// <param name="height">字符图片高度</param>
    /// <param name="xoffset">渲染字体时x方向的调整, 正表示向右, 负表示向左</param>
    /// <param name="yoffset">渲染字体时y方向的调整, 基于行top调整(正表示向下, 负表示向上)</param>
    /// <param name="xadvance">渲染字体时字体的宽度, 下一个字符从x+=xadvance处开始</param>
    /// <param name="page">在哪张字体图片上</param>
    /// <returns></returns>
    private CharacterInfo CreateCharInfo(int id, int x, int y, int width, int height, int xoffset, int yoffset, int xadvance, int page = 0)
    {
        Rect uv = new Rect();
        uv.x = (float)x / m_ImageWidth + page;
        uv.y = (float)y / m_ImageHeight;
        uv.width = (float)width / m_ImageWidth;
        uv.height = (float)height / m_ImageHeight;
        //BMFont: 贴图原点在左上角, 以左上角为min右下角为max
        //Unity: 贴图原点在左下角, 以左下角为min右上角为max
        uv.y = 1f - uv.y - uv.height;

        Rect vert = new Rect();
        vert.xMin = xoffset;
        vert.xMax = xoffset + width;
#if UNITY_5_0 || UNITY_5_1 || UNITY_5_2
        // unity 5.0 can not support baseline for
        vert.yMin = -yoffset;
        vert.yMax = -yoffset - height;
#else
        //yoffset使用负: BMFont中正为向下调整, Unity中负为向下
        //这个版本的TextGenerator做了基线处理, 他会减掉Font.m_Ascent的值, 所以这里要先加上
        vert.yMin = -yoffset + m_LineBaseHeight; //左上角
        vert.yMax = -yoffset - height + m_LineBaseHeight; //右下角
#endif

        CharacterInfo charInfo = new CharacterInfo();
        charInfo.index = id;

#if UNITY_5_3_OR_NEWER || UNITY_5_3 || UNITY_5_2
        //对应uv
        charInfo.uvBottomLeft = new Vector2(uv.xMin, uv.yMin);
        charInfo.uvBottomRight = new Vector2(uv.xMax, uv.yMin);
        charInfo.uvTopLeft = new Vector2(uv.xMin, uv.yMax);
        charInfo.uvTopRight = new Vector2(uv.xMax, uv.yMax);

        //对应vert
        charInfo.minX = (int)vert.xMin;
        charInfo.maxX = (int)vert.xMax;
        charInfo.minY = (int)vert.yMax; //minY = yMax这么写没写错, 实际m_Font.GetCharacterInfo('A', out var charInfo);还是拿到的左下角, 可能内部处理了
        charInfo.maxY = (int)vert.yMin;
        charInfo.bearing = (int)vert.x;

        //对应width
        charInfo.advance = xadvance;
#else
#pragma warning disable 618
          charInfo.uv = uv;
          charInfo.vert = vert;
          charInfo.width = xadvance;
#pragma warning restore 618
#endif
        return charInfo;
    }
}

 

注意

 1) 如果这边不加m_LineBaseHeight会怎样?

//这个版本的TextGenerator做了基线处理, 他会减掉Font.m_Ascent的值
vert.yMin = -yoffset; //左上角
vert.yMax = -yoffset - height; //右下角

因为TextGenerator做了基线处理减掉了Font.m_Ascent,所以文字偏下了

 

 2) 如果把yoffset调整拿掉会怎样?

//这个版本的TextGenerator做了基线处理, 他会减掉Font.m_Ascent的值, 所以这里要先加上
vert.yMin = 0 + m_LineBaseHeight; //左上角
vert.yMax = 0 - height + m_LineBaseHeight; //右下角

变成按顶部对齐了;所以,文字要正常显示:首先是往下移动ascent的距离,让文字按顶部对齐

然后再往下移动yoffset,让文字按baseline对齐。

 

参考

GitHub - litefeel/Unity-BitmapFontImporter: An unity editor extension for bitmap font.

 

posted @ 2023-06-28 23:30  yanghui01  阅读(67)  评论(0编辑  收藏  举报