美文网首页Unity技术分享unity优化Unity基础入门分享
[Unity Editor] 基于BMFont创建美术(静态)字

[Unity Editor] 基于BMFont创建美术(静态)字

作者: _Walker__ | 来源:发表于2017-12-21 23:09 被阅读82次

    1、背景

      马上要做战斗跳字了,提前调研下美术字的制作。
      使用美术字可以实现很好表现效果,并获得不错的性能,比如:规避掉描边、Shadow的消耗。

    2、BMFont使用

      参考文章中写的已经很详细了,这里仅列举几个不易发现的地方备忘。

    • 支持中文
      [Options - Font settings] Charset处,勾选Unicode(虽然它是默认选中的)。

    • 设置导出纹理格式
      [Options - Export options] Textures处,可以指定导出纹理为PNG。
      在物理存储上PNG比Tga小很多,而且在Unity中导入的效率也会更高。

    • 查看字符码
      界面上选中字符,在右下角的数字中m:nm就是字符码

      字符码
    • 导入字符的Icon
      这里只想说,它这按钮做的太不明显了Orz

      导入Icon

    3、创建Unity的CustomFont

    3.1 BMFontTool

      先给出工具的完整代码,要说明的点后面慢慢写。

    public class BMFontTool
    {
        public static Regex BMCharMatch =
            new Regex(
                @"char id=(?<id>\d+)\s+x=(?<x>\d+)\s+y=(?<y>\d+)\s+width=(?<width>\d+)\s+height=(?<height>\d+)\s+xoffset=(?<xoffset>\d+)\s+yoffset=(?<yoffset>\d+)\s+xadvance=(?<xadvance>\d+)\s+");
    
        public static Regex BMInfoMatch =
            new Regex(@"common lineHeight=(?<lineHeight>\d+)\s+.*scaleW=(?<scaleW>\d+)\s+scaleH=(?<scaleH>\d+)");
    
        public const string BMFontExt = ".fnt";
        public const string FontExt = ".fontsettings";
    
        public static string GetConfPath()
        {
            Object obj = Selection.activeObject;
            string cfgPath = AssetDatabase.GetAssetPath(obj);
            if (!cfgPath.EndsWith(".fnt"))
            {
                Debug.LogError("请选择.fnt文件!!");
                return null;
            }
            return cfgPath;
        }
    
        [MenuItem("Assets/Create Font")]
        public static void CreateFromBMFont()
        {
            string cfgPath = GetConfPath();
            if (null == cfgPath) return;
    
            string name = Path.GetFileNameWithoutExtension(cfgPath);
            int lineHeight = 1;
            // 创建材质
            Material mat = new Material(Shader.Find("UI/Unlit/Text Detail"))
            {
                name = name,
                mainTexture = AssetDatabase.LoadAssetAtPath<Texture>(cfgPath.Replace(BMFontExt, ".png")),
            };
            mat.SetTexture("_DetailTex", mat.mainTexture);
            // 创建字体
            Font customFont = new Font(name)
            {
                material = mat,
                characterInfo = ParseBMFont(cfgPath, ref lineHeight).ToArray(),
            };
            // 修改行高
            SerializedObject serializedFont = new SerializedObject(customFont);
            SetLineHeight(serializedFont, lineHeight);
            serializedFont.ApplyModifiedProperties();
            // 保存
            AssetDatabase.CreateAsset(mat, cfgPath.Replace(BMFontExt, ".mat"));
            AssetDatabase.CreateAsset(customFont, cfgPath.Replace(BMFontExt, FontExt));
        }
    
        [MenuItem("Assets/Build Font")]
        public static void BuildFromBMFont()
        {
            string cfgPath = GetConfPath();
            if (null == cfgPath) return;
    
            string fontPath = cfgPath.Replace(BMFontExt, FontExt);
            if (!File.Exists(fontPath)) return;
    
            Font customFont = AssetDatabase.LoadAssetAtPath<Font>(fontPath);
            int lineHeight = 1;
            List<CharacterInfo> chars = ParseBMFont(cfgPath, ref lineHeight);
            SerializeFont(customFont, chars, lineHeight);
            Debug.Log("字体更新完成", customFont);
        }
    
        public static List<CharacterInfo> ParseBMFont(string path, ref int lineHeight)
        {
            List<CharacterInfo> chars = new List<CharacterInfo>();
            using (StreamReader reader = new StreamReader(path))
            {
                // 文字贴图的宽、高
                float texWidth = 1;
                float texHeight = 1;
    
                string line = reader.ReadLine();
                while (line != null)
                {
                    if (line.StartsWith("char id="))
                    {
                        Match match = BMCharMatch.Match(line);
                        if (match != Match.Empty)
                        {
                            int id = Convert.ToInt32(match.Groups["id"].Value);
                            int x = Convert.ToInt32(match.Groups["x"].Value);
                            int y = Convert.ToInt32(match.Groups["y"].Value);
                            int width = Convert.ToInt32(match.Groups["width"].Value);
                            int height = Convert.ToInt32(match.Groups["height"].Value);
                            int xoffset = Convert.ToInt32(match.Groups["xoffset"].Value);
                            int yoffset = Convert.ToInt32(match.Groups["yoffset"].Value);
                            int xadvance = Convert.ToInt32(match.Groups["xadvance"].Value);
                            // 转换为Unity UV坐标
                            float uvMinX = x / texWidth;
                            float uvMaxX = (x + width) / texWidth;
                            float uvMaxY = 1 - (y / texHeight);
                            float uvMinY = (texHeight - height - y) / texHeight;
    
                            // Unity字体UV的是 [左下(0, 0) - 右上(1, 1)]
                            // BMFont的UV是 [左上(0,0) - 右下(1, 1)]
                            CharacterInfo info = new CharacterInfo
                            {
                                // 字符的Unicode值
                                index = id,
                                uvBottomLeft = new Vector2(uvMinX, uvMinY),
                                uvBottomRight = new Vector2(uvMaxX, uvMinY),
                                uvTopLeft = new Vector2(uvMinX, uvMaxY),
                                uvTopRight = new Vector2(uvMaxX, uvMaxY),
                                minX = xoffset,
                                minY = -height / 2, // 居中对齐
                                glyphWidth = width,
                                glyphHeight = height,
                                // The horizontal distance from the origin of this character to the origin of the next character.
                                advance = xadvance,
                            };
                            chars.Add(info);
                        }
                    }
                    else if (line.IndexOf("scaleW=", StringComparison.Ordinal) != -1)
                    {
                        Match match = BMInfoMatch.Match(line);
                        if (match != Match.Empty)
                        {
                            lineHeight = Convert.ToInt32(match.Groups["lineHeight"].Value);
                            texWidth = Convert.ToInt32(match.Groups["scaleW"].Value);
                            texHeight = Convert.ToInt32(match.Groups["scaleH"].Value);
                        }
                    }
                    line = reader.ReadLine();
                }
            }
            return chars;
        }
    
        public static void SetLineHeight(SerializedObject font, float height)
        {
            font.FindProperty("m_LineSpacing").floatValue = height;
        }
    
        /// <summary>
        /// 序列化自定义字体
        /// </summary>
        /// <param name="font">字体资源</param>
        /// <param name="chars">全部字符信息</param>
        /// <param name="lineHeight">显示的行高</param>
        public static SerializedObject SerializeFont(Font font, List<CharacterInfo> chars, float lineHeight)
        {
            SerializedObject serializedFont = new SerializedObject(font);
            SetLineHeight(serializedFont, lineHeight);
            SerializeFontCharInfos(serializedFont, chars);
            serializedFont.ApplyModifiedProperties();
            return serializedFont;
        }
    
        /// <summary>
        /// 序列化字体中的全部字符信息
        /// </summary>
        public static void SerializeFontCharInfos(SerializedObject font, List<CharacterInfo> chars)
        {
            SerializedProperty charRects = font.FindProperty("m_CharacterRects");
            charRects.arraySize = chars.Count;
            for (int i = 0; i < chars.Count; ++i)
            {
                CharacterInfo info = chars[i];
                SerializedProperty prop = charRects.GetArrayElementAtIndex(i);
                SerializeCharInfo(prop, info);
            }
        }
    
        /// <summary>
        /// 序列化一个字符信息
        /// </summary>
        public static void SerializeCharInfo(SerializedProperty prop, CharacterInfo charInfo)
        {
            prop.FindPropertyRelative("index").intValue = charInfo.index;
            prop.FindPropertyRelative("uv").rectValue = charInfo.uv;
            prop.FindPropertyRelative("vert").rectValue = charInfo.vert;
            prop.FindPropertyRelative("advance").floatValue = charInfo.advance;
            prop.FindPropertyRelative("flipped").boolValue = false;
        }
    }
    

    3.2 细节说明

      上面代码的核心部分,来自于参考文章,当然也有自己的补充和修改内容。

    3.2.1 解决代码生成的CustomFont信息会丢失问题

      参考文章实现了创建CustomFont的功能,但是需要手动保存(Ctrl+S),否则在关闭Unity或切换场景后,字体中的信息就丢失了。
      丢失的原因是,直接修改Load出来的Font对象,并不会将修改的内容序列化保存。这里我使用SerializedObject对字体进行修改,它可以通过ApplyModifiedProperties更新序列化的内容。
      另外代码中使用new Font() + AssetDatabase.CreateAsset()也可以保留序列化信息,但它只能用在创建字体时。更新字体用它会删掉旧字体,并创建一个新的,那么项目中用到旧字体的地方就会Missing。

    3.2.2 文字无法换行

    换行错乱
      上图的现象是没有设置行高导致的。Inspector中行高的参数是Line Spacing,而在代码中是Font.lineHeight。这里需要注意lineHeight是只读属性,想在代码里设置行高,只能借助SerializedObject。

    3.2.3 竖直方向上的文字对齐

      代码中保证了竖直居中对齐是正确的,但是上对齐、下对齐的位置会出现问题。我探索出了在三种对齐方式下分别正确的参数,但是不能同时满足三种对齐 Orz...

    minY或maxY只需要设置一个即可

    • 上对齐 : minY = -height/2 - yoffset*2
    • 居中对齐: minY = -height / 2
    • 下对齐 : maxY = height-yoffset

      虽然从参数上不能同时适配三种对齐,但勾选Text组件的Align By Geometry选项可以解决问题。
      如果有朋友解决了这个问题,请不吝赐教。还有minX、minY、maxX、maxY这几个参数的含义我也没搞懂,文档的信息实在是不清不楚 ……了解的朋友也请留言。

    相关文章

      网友评论

        本文标题:[Unity Editor] 基于BMFont创建美术(静态)字

        本文链接:https://www.haomeiwen.com/subject/sjczwxtx.html