• Unity3d之-使用BMFont制作美术字体


    一、需求

    游戏开发中经常遇到需要以美术字(而非字库)做数字显示的情况,通常美术会提供一组包含单个数字(也会有其它字符)的图片,可能是一张整图,也可能是每个数字分开的散图。

    在此我以一张整图这种情况为例,来说明美术字体的具体制作流程。整图如下:

    二、准备

    整个制作过程需要用到三样工具:

    • 字体数据制作工具
    • 图片切割工具
    • 字体生成工具

    1、字体数据制作工具

    字体数据制作工具名为BMFont,是一个Windows上的可执行软件,下载网址为:http://www.angelcode.com/products/bmfont/

      

      这里选择下载64位运行版(单体文件,无需安装)

      可也以点这里下载:BMFont64.exe

    2、图片切割工具

    图片切割工具是Unity中运行的一个工具类,类名为ImageSlicer,放在Editor目录下即可,代码如下:

     1 /**
     2 * UnityVersion: 2018.3.10f1
     3 * FileName:     ImageSlicer.cs
     4 * Author:       TYQ
     5 * CreateTime:   2019/04/19 00:04:26
     6 * Description:  
     7 */
     8 /*
     9 * Author:
    10 * Date:2019/01/30 10:24:22 
    11 * Desc:图集切割器 (针对Multiple格式的图片)
    12 * 操作方式:选中图片,选择编辑器的 Assets/ImageSlicer/Process to Sprites菜单
    13 */
    14 
    15 using UnityEngine;
    16 using System.Collections;
    17 using UnityEditor;
    18 using System.IO;
    19 using System.Collections.Generic;
    20 
    21 public static class ImageSlicer
    22 {
    23     [MenuItem("Assets/ImageSlicer/Process to Sprites")]
    24     static void ProcessToSprite()
    25     {
    26         Texture2D image = Selection.activeObject as Texture2D;//获取旋转的对象
    27         string rootPath = Path.GetDirectoryName(AssetDatabase.GetAssetPath(image));//获取路径名称
    28         string path = rootPath + "/" + image.name + ".PNG";//图片路径名称
    29 
    30 
    31         TextureImporter texImp = AssetImporter.GetAtPath(path) as TextureImporter;//获取图片入口
    32 
    33 
    34         AssetDatabase.CreateFolder(rootPath, image.name);//创建文件夹
    35 
    36 
    37         foreach (SpriteMetaData metaData in texImp.spritesheet)//遍历小图集
    38         {
    39             Texture2D myimage = new Texture2D((int)metaData.rect.width, (int)metaData.rect.height);
    40 
    41             //abc_0:(x:2.00, y:400.00, 103.00, height:112.00)
    42             for (int y = (int)metaData.rect.y; y < metaData.rect.y + metaData.rect.height; y++)//Y轴像素
    43             {
    44                 for (int x = (int)metaData.rect.x; x < metaData.rect.x + metaData.rect.width; x++)
    45                     myimage.SetPixel(x - (int)metaData.rect.x, y - (int)metaData.rect.y, image.GetPixel(x, y));
    46             }
    47 
    48 
    49             //转换纹理到EncodeToPNG兼容格式
    50             if (myimage.format != TextureFormat.ARGB32 && myimage.format != TextureFormat.RGB24)
    51             {
    52                 Texture2D newTexture = new Texture2D(myimage.width, myimage.height);
    53                 newTexture.SetPixels(myimage.GetPixels(0), 0);
    54                 myimage = newTexture;
    55             }
    56             var pngData = myimage.EncodeToPNG();
    57 
    58 
    59             //AssetDatabase.CreateAsset(myimage, rootPath + "/" + image.name + "/" + metaData.name + ".PNG");
    60             File.WriteAllBytes(rootPath + "/" + image.name + "/" + metaData.name + ".PNG", pngData);
    61             // 刷新资源窗口界面
    62             AssetDatabase.Refresh();
    63         }
    64     }
    65 }
    ImageSlicer.cs

    编译完成后会在Assets菜单下生成一个ImageSlicer/Process to Sprites的菜单项,选中图片然后右键也可以看到。

     3、字体生成工具

    字体生成工具也是Unity3d中一个第三方插件,名字也是BMFont(不知道和第一个软件有什么关联)。原本是NGUI中的一个字体制作工具,现被大佬剥离出来,在UGUI中也可以使用。

     下载地址:BMFont字体生成工具

     解压到Assets目录下即可,编译完成后,会在Unity编辑器上生成一个Tools/BMFont Maker菜单。

    BMFont插件是源码形式的,共包括6个文件:

    //----------------------------------------------
    //            NGUI: Next-Gen UI kit
    // Copyright © 2011-2015 Tasharen Entertainment
    //----------------------------------------------
    
    using UnityEngine;
    using System.Collections.Generic;
    
    /// <summary>
    /// BMFont reader. C# implementation of http://www.angelcode.com/products/bmfont/
    /// </summary>
    
    [System.Serializable]
    public class BMFont
    {
        [HideInInspector][SerializeField] int mSize = 16;            // How much to move the cursor when moving to the next line
        [HideInInspector][SerializeField] int mBase = 0;            // Offset from the top of the line to the base of each character
        [HideInInspector][SerializeField] int mWidth = 0;            // Original width of the texture
        [HideInInspector][SerializeField] int mHeight = 0;            // Original height of the texture
        [HideInInspector][SerializeField] string mSpriteName;
    
        // List of serialized glyphs
        [HideInInspector][SerializeField] List<BMGlyph> mSaved = new List<BMGlyph>();
    
        // Actual glyphs that we'll be working with are stored in a dictionary, making the lookup faster
        Dictionary<int, BMGlyph> mDict = new Dictionary<int, BMGlyph>();
    
        /// <summary>
        /// Whether the font can be used.
        /// </summary>
    
        public bool isValid { get { return (mSaved.Count > 0); } }
    
        /// <summary>
        /// Size of this font (for example 32 means 32 pixels).
        /// </summary>
    
        public int charSize { get { return mSize; } set { mSize = value; } }
    
        /// <summary>
        /// Base offset applied to characters.
        /// </summary>
    
        public int baseOffset { get { return mBase; } set { mBase = value; } }
    
        /// <summary>
        /// Original width of the texture.
        /// </summary>
    
        public int texWidth { get { return mWidth; } set { mWidth = value; } }
    
        /// <summary>
        /// Original height of the texture.
        /// </summary>
    
        public int texHeight { get { return mHeight; } set { mHeight = value; } }
    
        /// <summary>
        /// Number of valid glyphs.
        /// </summary>
    
        public int glyphCount { get { return isValid ? mSaved.Count : 0; } }
    
        /// <summary>
        /// Original name of the sprite that the font is expecting to find (usually the name of the texture).
        /// </summary>
    
        public string spriteName { get { return mSpriteName; } set { mSpriteName = value; } }
    
        /// <summary>
        /// Access to BMFont's entire set of glyphs.
        /// </summary>
    
        public List<BMGlyph> glyphs { get { return mSaved; } }
    
        /// <summary>
        /// Helper function that retrieves the specified glyph, creating it if necessary.
        /// </summary>
    
        public BMGlyph GetGlyph (int index, bool createIfMissing)
        {
            // Get the requested glyph
            BMGlyph glyph = null;
    
            if (mDict.Count == 0)
            {
                // Populate the dictionary for faster access
                for (int i = 0, imax = mSaved.Count; i < imax; ++i)
                {
                    BMGlyph bmg = mSaved[i];
                    mDict.Add(bmg.index, bmg);
                }
            }
    
            // Saved check is here so that the function call is not needed if it's true
            if (!mDict.TryGetValue(index, out glyph) && createIfMissing)
            {
                glyph = new BMGlyph();
                glyph.index = index;
                mSaved.Add(glyph);
                mDict.Add(index, glyph);
            }
            return glyph;
        }
    
        /// <summary>
        /// Retrieve the specified glyph, if it's present.
        /// </summary>
    
        public BMGlyph GetGlyph (int index) { return GetGlyph(index, false); }
    
        /// <summary>
        /// Clear the glyphs.
        /// </summary>
    
        public void Clear ()
        {
            mDict.Clear();
            mSaved.Clear();
        }
    
        /// <summary>
        /// Trim the glyphs, ensuring that they will never go past the specified bounds.
        /// </summary>
    
        public void Trim (int xMin, int yMin, int xMax, int yMax)
        {
            if (isValid)
            {
                for (int i = 0, imax = mSaved.Count; i < imax; ++i)
                {
                    BMGlyph glyph = mSaved[i];
                    if (glyph != null) glyph.Trim(xMin, yMin, xMax, yMax);
                }
            }
        }
    }
    1.BMFont.cs
    using UnityEngine;
    using UnityEditor;
    
    public class BMFontEditor : EditorWindow
    {
        [MenuItem("Tools/BMFont Maker")]
        static public void OpenBMFontMaker()
        {
            EditorWindow.GetWindow<BMFontEditor>(false, "BMFont Maker", true).Show();
        }
    
        [SerializeField]
        private Font targetFont;
        [SerializeField]
        private TextAsset fntData;
        [SerializeField]
        private Material fontMaterial;
        [SerializeField]
        private Texture2D fontTexture;
    
        private BMFont bmFont = new BMFont();
    
        public BMFontEditor()
        {
        }
    
        void OnGUI()
        {
            targetFont = EditorGUILayout.ObjectField("Target Font", targetFont, typeof(Font), false) as Font;
            fntData = EditorGUILayout.ObjectField("Fnt Data", fntData, typeof(TextAsset), false) as TextAsset;
            fontMaterial = EditorGUILayout.ObjectField("Font Material", fontMaterial, typeof(Material), false) as Material;
            fontTexture = EditorGUILayout.ObjectField("Font Texture", fontTexture, typeof(Texture2D), false) as Texture2D;
    
            if (GUILayout.Button("Create BMFont"))
            {
                BMFontReader.Load(bmFont, fntData.name, fntData.bytes); // 借用NGUI封装的读取类
                CharacterInfo[] characterInfo = new CharacterInfo[bmFont.glyphs.Count];
                for (int i = 0; i < bmFont.glyphs.Count; i++)
                {
                    BMGlyph bmInfo = bmFont.glyphs[i];
                    CharacterInfo info = new CharacterInfo();
                    info.index = bmInfo.index;
                    info.uv.x = (float)bmInfo.x / (float)bmFont.texWidth;
                    info.uv.y = 1 - (float)bmInfo.y / (float)bmFont.texHeight;
                    info.uv.width = (float)bmInfo.width / (float)bmFont.texWidth;
                    info.uv.height = -1f * (float)bmInfo.height / (float)bmFont.texHeight;
                    info.vert.x = 0;
                    info.vert.y = -(float)bmInfo.height;
                    info.vert.width = (float)bmInfo.width;
                    info.vert.height = (float)bmInfo.height;
                    info.width = (float)bmInfo.advance;
                    characterInfo[i] = info;
                }
                targetFont.characterInfo = characterInfo;
                if (fontMaterial)
                {
                    fontMaterial.mainTexture = fontTexture;
                }
                targetFont.material = fontMaterial;
                fontMaterial.shader = Shader.Find("UI/Default");
    
                //偶遇字体信息在重启后失丢失的情况,需要加此句
                EditorUtility.SetDirty(targetFont);
    
                Debug.Log("create font <" + targetFont.name + "> success");
                Close();
            }
        }
    }
    2.BMFontEditor.cs
    //----------------------------------------------
    //            NGUI: Next-Gen UI kit
    // Copyright © 2011-2015 Tasharen Entertainment
    //----------------------------------------------
    
    using UnityEngine;
    using System.Collections.Generic;
    using System.Diagnostics;
    
    /// <summary>
    /// This improved version of the System.Collections.Generic.List that doesn't release the buffer on Clear(),
    /// resulting in better performance and less garbage collection.
    /// PRO: BetterList performs faster than List when you Add and Remove items (although slower if you remove from the beginning).
    /// CON: BetterList performs worse when sorting the list. If your operations involve sorting, use the standard List instead.
    /// </summary>
    
    public class BetterList<T>
    {
    #if UNITY_FLASH
    
        List<T> mList = new List<T>();
        
        /// <summary>
        /// Direct access to the buffer. Note that you should not use its 'Length' parameter, but instead use BetterList.size.
        /// </summary>
        
        public T this[int i]
        {
            get { return mList[i]; }
            set { mList[i] = value; }
        }
        
        /// <summary>
        /// Compatibility with the non-flash syntax.
        /// </summary>
        
        public List<T> buffer { get { return mList; } }
    
        /// <summary>
        /// Direct access to the buffer's size. Note that it's only public for speed and efficiency. You shouldn't modify it.
        /// </summary>
    
        public int size { get { return mList.Count; } }
    
        /// <summary>
        /// For 'foreach' functionality.
        /// </summary>
    
        public IEnumerator<T> GetEnumerator () { return mList.GetEnumerator(); }
    
        /// <summary>
        /// Clear the array by resetting its size to zero. Note that the memory is not actually released.
        /// </summary>
    
        public void Clear () { mList.Clear(); }
    
        /// <summary>
        /// Clear the array and release the used memory.
        /// </summary>
    
        public void Release () { mList.Clear(); }
    
        /// <summary>
        /// Add the specified item to the end of the list.
        /// </summary>
    
        public void Add (T item) { mList.Add(item); }
    
        /// <summary>
        /// Insert an item at the specified index, pushing the entries back.
        /// </summary>
    
        public void Insert (int index, T item)
        {
            if (index > -1 && index < mList.Count) mList.Insert(index, item);
            else mList.Add(item);
        }
    
        /// <summary>
        /// Returns 'true' if the specified item is within the list.
        /// </summary>
    
        public bool Contains (T item) { return mList.Contains(item); }
    
        /// <summary>
        /// Return the index of the specified item.
        /// </summary>
    
        public int IndexOf (T item) { return mList.IndexOf(item); }
    
        /// <summary>
        /// Remove the specified item from the list. Note that RemoveAt() is faster and is advisable if you already know the index.
        /// </summary>
    
        public bool Remove (T item) { return mList.Remove(item); }
    
        /// <summary>
        /// Remove an item at the specified index.
        /// </summary>
    
        public void RemoveAt (int index) { mList.RemoveAt(index); }
    
        /// <summary>
        /// Remove an item from the end.
        /// </summary>
    
        public T Pop ()
        {
            if (buffer != null && size != 0)
            {
                T val = buffer[mList.Count - 1];
                mList.RemoveAt(mList.Count - 1);
                return val;
            }
            return default(T);
        }
    
        /// <summary>
        /// Mimic List's ToArray() functionality, except that in this case the list is resized to match the current size.
        /// </summary>
    
        public T[] ToArray () { return mList.ToArray(); }
    
        /// <summary>
        /// List.Sort equivalent.
        /// </summary>
    
        public void Sort (System.Comparison<T> comparer) { mList.Sort(comparer); }
    
    #else
    
        /// <summary>
        /// Direct access to the buffer. Note that you should not use its 'Length' parameter, but instead use BetterList.size.
        /// </summary>
    
        public T[] buffer;
    
        /// <summary>
        /// Direct access to the buffer's size. Note that it's only public for speed and efficiency. You shouldn't modify it.
        /// </summary>
    
        public int size = 0;
    
        /// <summary>
        /// For 'foreach' functionality.
        /// </summary>
    
        [DebuggerHidden]
        [DebuggerStepThrough]
        public IEnumerator<T> GetEnumerator ()
        {
            if (buffer != null)
            {
                for (int i = 0; i < size; ++i)
                {
                    yield return buffer[i];
                }
            }
        }
        
        /// <summary>
        /// Convenience function. I recommend using .buffer instead.
        /// </summary>
    
        [DebuggerHidden]
        public T this[int i]
        {
            get { return buffer[i]; }
            set { buffer[i] = value; }
        }
    
        /// <summary>
        /// Helper function that expands the size of the array, maintaining the content.
        /// </summary>
    
        void AllocateMore ()
        {
            T[] newList = (buffer != null) ? new T[Mathf.Max(buffer.Length << 1, 32)] : new T[32];
            if (buffer != null && size > 0) buffer.CopyTo(newList, 0);
            buffer = newList;
        }
    
        /// <summary>
        /// Trim the unnecessary memory, resizing the buffer to be of 'Length' size.
        /// Call this function only if you are sure that the buffer won't need to resize anytime soon.
        /// </summary>
    
        void Trim ()
        {
            if (size > 0)
            {
                if (size < buffer.Length)
                {
                    T[] newList = new T[size];
                    for (int i = 0; i < size; ++i) newList[i] = buffer[i];
                    buffer = newList;
                }
            }
            else buffer = null;
        }
    
        /// <summary>
        /// Clear the array by resetting its size to zero. Note that the memory is not actually released.
        /// </summary>
    
        public void Clear () { size = 0; }
    
        /// <summary>
        /// Clear the array and release the used memory.
        /// </summary>
    
        public void Release () { size = 0; buffer = null; }
    
        /// <summary>
        /// Add the specified item to the end of the list.
        /// </summary>
    
        public void Add (T item)
        {
            if (buffer == null || size == buffer.Length) AllocateMore();
            buffer[size++] = item;
        }
    
        /// <summary>
        /// Insert an item at the specified index, pushing the entries back.
        /// </summary>
    
        public void Insert (int index, T item)
        {
            if (buffer == null || size == buffer.Length) AllocateMore();
    
            if (index > -1 && index < size)
            {
                for (int i = size; i > index; --i) buffer[i] = buffer[i - 1];
                buffer[index] = item;
                ++size;
            }
            else Add(item);
        }
    
        /// <summary>
        /// Returns 'true' if the specified item is within the list.
        /// </summary>
    
        public bool Contains (T item)
        {
            if (buffer == null) return false;
            for (int i = 0; i < size; ++i) if (buffer[i].Equals(item)) return true;
            return false;
        }
    
        /// <summary>
        /// Return the index of the specified item.
        /// </summary>
    
        public int IndexOf (T item)
        {
            if (buffer == null) return -1;
            for (int i = 0; i < size; ++i) if (buffer[i].Equals(item)) return i;
            return -1;
        }
    
        /// <summary>
        /// Remove the specified item from the list. Note that RemoveAt() is faster and is advisable if you already know the index.
        /// </summary>
    
        public bool Remove (T item)
        {
            if (buffer != null)
            {
                EqualityComparer<T> comp = EqualityComparer<T>.Default;
    
                for (int i = 0; i < size; ++i)
                {
                    if (comp.Equals(buffer[i], item))
                    {
                        --size;
                        buffer[i] = default(T);
                        for (int b = i; b < size; ++b) buffer[b] = buffer[b + 1];
                        buffer[size] = default(T);
                        return true;
                    }
                }
            }
            return false;
        }
    
        /// <summary>
        /// Remove an item at the specified index.
        /// </summary>
    
        public void RemoveAt (int index)
        {
            if (buffer != null && index > -1 && index < size)
            {
                --size;
                buffer[index] = default(T);
                for (int b = index; b < size; ++b) buffer[b] = buffer[b + 1];
                buffer[size] = default(T);
            }
        }
    
        /// <summary>
        /// Remove an item from the end.
        /// </summary>
    
        public T Pop ()
        {
            if (buffer != null && size != 0)
            {
                T val = buffer[--size];
                buffer[size] = default(T);
                return val;
            }
            return default(T);
        }
    
        /// <summary>
        /// Mimic List's ToArray() functionality, except that in this case the list is resized to match the current size.
        /// </summary>
    
        public T[] ToArray () { Trim(); return buffer; }
    
        //class Comparer : System.Collections.IComparer
        //{
        //    public System.Comparison<T> func;
        //    public int Compare (object x, object y) { return func((T)x, (T)y); }
        //}
    
        //Comparer mComp = new Comparer();
    
        /// <summary>
        /// List.Sort equivalent. Doing Array.Sort causes GC allocations.
        /// </summary>
    
        //public void Sort (System.Comparison<T> comparer)
        //{
        //    if (size > 0)
        //    {
        //        mComp.func = comparer;
        //        System.Array.Sort(buffer, 0, size, mComp);
        //    }
        //}
    
        /// <summary>
        /// List.Sort equivalent. Manual sorting causes no GC allocations.
        /// </summary>
    
        [DebuggerHidden]
        [DebuggerStepThrough]
        public void Sort (CompareFunc comparer)
        {
            int start = 0;
            int max = size - 1;
            bool changed = true;
    
            while (changed)
            {
                changed = false;
    
                for (int i = start; i < max; ++i)
                {
                    // Compare the two values
                    if (comparer(buffer[i], buffer[i + 1]) > 0)
                    {
                        // Swap the values
                        T temp = buffer[i];
                        buffer[i] = buffer[i + 1];
                        buffer[i + 1] = temp;
                        changed = true;
                    }
                    else if (!changed)
                    {
                        // Nothing has changed -- we can start here next time
                        start = (i == 0) ? 0 : i - 1;
                    }
                }
            }
        }
    
        /// <summary>
        /// Comparison function should return -1 if left is less than right, 1 if left is greater than right, and 0 if they match.
        /// </summary>
    
        public delegate int CompareFunc (T left, T right);
    #endif
    }
    3.BetterList.cs
    //----------------------------------------------
    //            NGUI: Next-Gen UI kit
    // Copyright © 2011-2015 Tasharen Entertainment
    //----------------------------------------------
    
    using UnityEngine;
    using UnityEditor;
    using System.Text;
    
    /// <summary>
    /// Helper class that takes care of loading BMFont's glyph information from the specified byte array.
    /// This functionality is not a part of BMFont anymore because Flash export option can't handle System.IO functions.
    /// </summary>
    
    public static class BMFontReader
    {
        /// <summary>
        /// Helper function that retrieves the string value of the key=value pair.
        /// </summary>
    
        static string GetString (string s)
        {
            int idx = s.IndexOf('=');
            return (idx == -1) ? "" : s.Substring(idx + 1);
        }
    
        /// <summary>
        /// Helper function that retrieves the integer value of the key=value pair.
        /// </summary>
    
        static int GetInt (string s)
        {
            int val = 0;
            string text = GetString(s);
    #if UNITY_FLASH
            try { val = int.Parse(text); } catch (System.Exception) { }
    #else
            int.TryParse(text, out val);
    #endif
            return val;
        }
    
        /// <summary>
        /// Reload the font data.
        /// </summary>
    
        static public void Load (BMFont font, string name, byte[] bytes)
        {
            font.Clear();
    
            if (bytes != null)
            {
                ByteReader reader = new ByteReader(bytes);
                char[] separator = new char[] { ' ' };
    
                while (reader.canRead)
                {
                    string line = reader.ReadLine();
                    if (string.IsNullOrEmpty(line)) break;
                    string[] split = line.Split(separator, System.StringSplitOptions.RemoveEmptyEntries);
                    int len = split.Length;
    
                    if (split[0] == "char")
                    {
                        // Expected data style:
                        // char id=13 x=506 y=62 width=3 height=3 xoffset=-1 yoffset=50 xadvance=0 page=0 chnl=15
    
                        int channel = (len > 10) ? GetInt(split[10]) : 15;
    
                        if (len > 9 && GetInt(split[9]) > 0)
                        {
                            Debug.LogError("Your font was exported with more than one texture. Only one texture is supported by NGUI.
    " +
                                "You need to re-export your font, enlarging the texture's dimensions until everything fits into just one texture.");
                            break;
                        }
    
                        if (len > 8)
                        {
                            int id = GetInt(split[1]);
                            BMGlyph glyph = font.GetGlyph(id, true);
    
                            if (glyph != null)
                            {
                                glyph.x            = GetInt(split[2]);
                                glyph.y            = GetInt(split[3]);
                                glyph.width        = GetInt(split[4]);
                                glyph.height    = GetInt(split[5]);
                                glyph.offsetX    = GetInt(split[6]);
                                glyph.offsetY    = GetInt(split[7]);
                                glyph.advance    = GetInt(split[8]);
                                glyph.channel    = channel;
                            }
                            else Debug.Log("Char: " + split[1] + " (" + id + ") is NULL");
                        }
                        else
                        {
                            Debug.LogError("Unexpected number of entries for the 'char' field (" + name + ", " + split.Length + "):
    " + line);
                            break;
                        }
                    }
                    else if (split[0] == "kerning")
                    {
                        // Expected data style:
                        // kerning first=84 second=244 amount=-5 
    
                        if (len > 3)
                        {
                            int first  = GetInt(split[1]);
                            int second = GetInt(split[2]);
                            int amount = GetInt(split[3]);
    
                            BMGlyph glyph = font.GetGlyph(second, true);
                            if (glyph != null) glyph.SetKerning(first, amount);
                        }
                        else
                        {
                            Debug.LogError("Unexpected number of entries for the 'kerning' field (" +
                                name + ", " + split.Length + "):
    " + line);
                            break;
                        }
                    }
                    else if (split[0] == "common")
                    {
                        // Expected data style:
                        // common lineHeight=64 base=51 scaleW=512 scaleH=512 pages=1 packed=0 alphaChnl=1 redChnl=4 greenChnl=4 blueChnl=4
    
                        if (len > 5)
                        {
                            font.charSize    = GetInt(split[1]);
                            font.baseOffset = GetInt(split[2]);
                            font.texWidth    = GetInt(split[3]);
                            font.texHeight    = GetInt(split[4]);
    
                            int pages = GetInt(split[5]);
    
                            if (pages != 1)
                            {
                                Debug.LogError("Font '" + name + "' must be created with only 1 texture, not " + pages);
                                break;
                            }
                        }
                        else
                        {
                            Debug.LogError("Unexpected number of entries for the 'common' field (" +
                                name + ", " + split.Length + "):
    " + line);
                            break;
                        }
                    }
                    else if (split[0] == "page")
                    {
                        // Expected data style:
                        // page id=0 file="textureName.png"
    
                        if (len > 2)
                        {
                            font.spriteName = GetString(split[2]).Replace(""", "");
                            font.spriteName = font.spriteName.Replace(".png", "");
                            font.spriteName = font.spriteName.Replace(".tga", "");
                        }
                    }
                }
            }
        }
    }
    4.BMFontReader.cs
    //----------------------------------------------
    //            NGUI: Next-Gen UI kit
    // Copyright © 2011-2015 Tasharen Entertainment
    //----------------------------------------------
    
    using UnityEngine;
    using System.Collections.Generic;
    
    /// <summary>
    /// Glyph structure used by BMFont. For more information see http://www.angelcode.com/products/bmfont/
    /// </summary>
    
    [System.Serializable]
    public class BMGlyph
    {
        public int index;    // Index of this glyph (used by BMFont)
        public int x;        // Offset from the left side of the texture to the left side of the glyph
        public int y;        // Offset from the top of the texture to the top of the glyph
        public int width;    // Glyph's width in pixels
        public int height;    // Glyph's height in pixels
        public int offsetX;    // Offset to apply to the cursor's left position before drawing this glyph
        public int offsetY; // Offset to apply to the cursor's top position before drawing this glyph
        public int advance;    // How much to move the cursor after printing this character
        public int channel;    // Channel mask (in most cases this will be 15 (RGBA, 1+2+4+8)
        public List<int> kerning;
    
        /// <summary>
        /// Retrieves the special amount by which to adjust the cursor position, given the specified previous character.
        /// </summary>
    
        public int GetKerning (int previousChar)
        {
            if (kerning != null && previousChar != 0)
            {
                for (int i = 0, imax = kerning.Count; i < imax; i += 2)
                    if (kerning[i] == previousChar)
                        return kerning[i + 1];
            }
            return 0;
        }
    
        /// <summary>
        /// Add a new kerning entry to the character (or adjust an existing one).
        /// </summary>
    
        public void SetKerning (int previousChar, int amount)
        {
            if (kerning == null) kerning = new List<int>();
    
            for (int i = 0; i < kerning.Count; i += 2)
            {
                if (kerning[i] == previousChar)
                {
                    kerning[i + 1] = amount;
                    return;
                }
            }
    
            kerning.Add(previousChar);
            kerning.Add(amount);
        }
    
        /// <summary>
        /// Trim the glyph, given the specified minimum and maximum dimensions in pixels.
        /// </summary>
    
        public void Trim (int xMin, int yMin, int xMax, int yMax)
        {
            int x1 = x + width;
            int y1 = y + height;
    
            if (x < xMin)
            {
                int offset = xMin - x;
                x += offset;
                width -= offset;
                offsetX += offset;
            }
    
            if (y < yMin)
            {
                int offset = yMin - y;
                y += offset;
                height -= offset;
                offsetY += offset;
            }
    
            if (x1 > xMax) width  -= x1 - xMax;
            if (y1 > yMax) height -= y1 - yMax;
        }
    }
    5.BMGlyph.cs
    //----------------------------------------------
    //            NGUI: Next-Gen UI kit
    // Copyright © 2011-2015 Tasharen Entertainment
    //----------------------------------------------
    
    using UnityEngine;
    using System.Text;
    using System.Collections.Generic;
    using System.IO;
    
    /// <summary>
    /// MemoryStream.ReadLine has an interesting oddity: it doesn't always advance the stream's position by the correct amount:
    /// http://social.msdn.microsoft.com/Forums/en-AU/Vsexpressvcs/thread/b8f7837b-e396-494e-88e1-30547fcf385f
    /// Solution? Custom line reader with the added benefit of not having to use streams at all.
    /// </summary>
    
    public class ByteReader
    {
        byte[] mBuffer;
        int mOffset = 0;
    
        public ByteReader (byte[] bytes) { mBuffer = bytes; }
        public ByteReader (TextAsset asset) { mBuffer = asset.bytes; }
    
        /// <summary>
        /// Read the contents of the specified file and return a Byte Reader to work with.
        /// </summary>
    
        static public ByteReader Open (string path)
        {
    #if UNITY_EDITOR || (!UNITY_FLASH && !NETFX_CORE && !UNITY_WP8 && !UNITY_WP_8_1)
            FileStream fs = File.OpenRead(path);
    
            if (fs != null)
            {
                fs.Seek(0, SeekOrigin.End);
                byte[] buffer = new byte[fs.Position];
                fs.Seek(0, SeekOrigin.Begin);
                fs.Read(buffer, 0, buffer.Length);
                fs.Close();
                return new ByteReader(buffer);
            }
    #endif
            return null;
        }
    
        /// <summary>
        /// Whether the buffer is readable.
        /// </summary>
    
        public bool canRead { get { return (mBuffer != null && mOffset < mBuffer.Length); } }
    
        /// <summary>
        /// Read a single line from the buffer.
        /// </summary>
    
        static string ReadLine (byte[] buffer, int start, int count)
        {
    #if UNITY_FLASH
            // Encoding.UTF8 is not supported in Flash :(
            StringBuilder sb = new StringBuilder();
    
            int max = start + count;
    
            for (int i = start; i < max; ++i)
            {
                byte byte0 = buffer[i];
    
                if ((byte0 & 128) == 0)
                {
                    // If an UCS fits 7 bits, its coded as 0xxxxxxx. This makes ASCII character represented by themselves
                    sb.Append((char)byte0);
                }
                else if ((byte0 & 224) == 192)
                {
                    // If an UCS fits 11 bits, it is coded as 110xxxxx 10xxxxxx
                    if (++i == count) break;
                    byte byte1 = buffer[i];
                    int ch = (byte0 & 31) << 6;
                    ch |= (byte1 & 63);
                    sb.Append((char)ch);
                }
                else if ((byte0 & 240) == 224)
                {
                    // If an UCS fits 16 bits, it is coded as 1110xxxx 10xxxxxx 10xxxxxx
                    if (++i == count) break;
                    byte byte1 = buffer[i];
                    if (++i == count) break;
                    byte byte2 = buffer[i];
    
                    if (byte0 == 0xEF && byte1 == 0xBB && byte2 == 0xBF)
                    {
                        // Byte Order Mark -- generally the first 3 bytes in a Windows-saved UTF-8 file. Skip it.
                    }
                    else
                    {
                        int ch = (byte0 & 15) << 12;
                        ch |= (byte1 & 63) << 6;
                        ch |= (byte2 & 63);
                        sb.Append((char)ch);
                    }
                }
                else if ((byte0 & 248) == 240)
                {
                    // If an UCS fits 21 bits, it is coded as 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 
                    if (++i == count) break;
                    byte byte1 = buffer[i];
                    if (++i == count) break;
                    byte byte2 = buffer[i];
                    if (++i == count) break;
                    byte byte3 = buffer[i];
    
                    int ch = (byte0 & 7) << 18;
                    ch |= (byte1 & 63) << 12;
                    ch |= (byte2 & 63) << 6;
                    ch |= (byte3 & 63);
                    sb.Append((char)ch);
                }
            }
            return sb.ToString();
    #else
            return Encoding.UTF8.GetString(buffer, start, count);
    #endif
        }
    
        /// <summary>
        /// Read a single line from the buffer.
        /// </summary>
    
        public string ReadLine () { return ReadLine(true); }
    
        /// <summary>
        /// Read a single line from the buffer.
        /// </summary>
    
        public string ReadLine (bool skipEmptyLines)
        {
            int max = mBuffer.Length;
    
            // Skip empty characters
            if (skipEmptyLines)
            {
                while (mOffset < max && mBuffer[mOffset] < 32) ++mOffset;
            }
    
            int end = mOffset;
    
            if (end < max)
            {
                for (; ; )
                {
                    if (end < max)
                    {
                        int ch = mBuffer[end++];
                        if (ch != '
    ' && ch != '
    ') continue;
                    }
                    else ++end;
    
                    string line = ReadLine(mBuffer, mOffset, end - mOffset - 1);
                    mOffset = end;
                    return line;
                }
            }
            mOffset = max;
            return null;
        }
    
        /// <summary>
        /// Assume that the entire file is a collection of key/value pairs.
        /// </summary>
    
        public Dictionary<string, string> ReadDictionary ()
        {
            Dictionary<string, string> dict = new Dictionary<string, string>();
            char[] separator = new char[] { '=' };
    
            while (canRead)
            {
                string line = ReadLine();
                if (line == null) break;
                if (line.StartsWith("//")) continue;
    
    #if UNITY_FLASH
                string[] split = line.Split(separator, System.StringSplitOptions.RemoveEmptyEntries);
    #else
                string[] split = line.Split(separator, 2, System.StringSplitOptions.RemoveEmptyEntries);
    #endif
    
                if (split.Length == 2)
                {
                    string key = split[0].Trim();
                    string val = split[1].Trim().Replace("\n", "
    ");
                    dict[key] = val;
                }
            }
            return dict;
        }
    
        static BetterList<string> mTemp = new BetterList<string>();
    
        /// <summary>
        /// Read a single line of Comma-Separated Values from the file.
        /// </summary>
    
        public BetterList<string> ReadCSV ()
        {
            mTemp.Clear();
            string line = "";
            bool insideQuotes = false;
            int wordStart = 0;
    
            while (canRead)
            {
                if (insideQuotes)
                {
                    string s = ReadLine(false);
                    if (s == null) return null;
                    s = s.Replace("\n", "
    ");
                    line += "
    " + s;
                }
                else
                {
                    line = ReadLine(true);
                    if (line == null) return null;
                    line = line.Replace("\n", "
    ");
                    wordStart = 0;
                }
    
                for (int i = wordStart, imax = line.Length; i < imax; ++i)
                {
                    char ch = line[i];
    
                    if (ch == ',')
                    {
                        if (!insideQuotes)
                        {
                            mTemp.Add(line.Substring(wordStart, i - wordStart));
                            wordStart = i + 1;
                        }
                    }
                    else if (ch == '"')
                    {
                        if (insideQuotes)
                        {
                            if (i + 1 >= imax)
                            {
                                mTemp.Add(line.Substring(wordStart, i - wordStart).Replace("""", """));
                                return mTemp;
                            }
    
                            if (line[i + 1] != '"')
                            {
                                mTemp.Add(line.Substring(wordStart, i - wordStart).Replace("""", """));
                                insideQuotes = false;
    
                                if (line[i + 1] == ',')
                                {
                                    ++i;
                                    wordStart = i + 1;
                                }
                            }
                            else ++i;
                        }
                        else
                        {
                            wordStart = i + 1;
                            insideQuotes = true;
                        }
                    }
                }
    
                if (wordStart < line.Length)
                {
                    if (insideQuotes) continue;
                    mTemp.Add(line.Substring(wordStart, line.Length - wordStart));
                }
                return mTemp;
            }
            return null;
        }
    }
    6.ByteReader.cs

    三、开始制作

    1、切割图片

    在字体数据制作软件BMFont64中,需要使用单个数字的图片,而我这个是一张包含所有数字和字母符号的整图,就需要切成单张散图。

    a) 把图片导入Unity,Sprite Mode选择Multiple模式,勾选Read/Write Enable选项。见下图:

    然后点击Sprite Editor进行多图区域编辑,如下图。可以先按给定的三种方式进行划分,自己再做细微调整。注意每个字符边距不要太大,不然做成字体后显示起来就会很离散。

     

    分割完成后,点击Apply保存操作。

    b) 选中图片右键,执行ImageSlicer/Process to Sprites菜单,会生成一个与图片同名的目录,里边放着切割好的散图。见下图,

    2、制作字体数据

    a) 打开BMFont64软件,点击Edit下的Open Image Manager菜单。

    在打开的Image Manager窗口有一个Image菜单,可以进行图片导入、编辑和删除操作。

    操作方式:这里以逗号字符为例,鼠标放在主窗口逗号方格的位置,右下会显示其编号,记住这个编号。

     

    然后在Image Manager窗口中选择导入图片,选中切割成散图的逗号图片,在Icon Image弹窗的Id中填入逗号方格的编号:44,点击Ok。

     

    依样导入其它的图片,并填入Id值,最后的完成图如下:每个字符方格的编号,对应一个相应的图片。

    b) 点击Options/Export options菜单,

     打开导出选项窗口,这里边主要设置一个合成图片的宽和高,以及导出格式。

    这个软件的最后一步操作是导出字体数据,包括一个字体数据文件(.fnt格式)和一张纹理图。这个纹理图会把所有的单图又合成一张。

    这里的Width是指这张合成纹理的总宽度(最好比所有图片加起来的数值要大一点,因为每个数字图片合成时会有一个px的间隔),

    Height是单个图片的高度(最好比图片高1像素以上)。

    不能一次设置准确也没关系,可以点击Options/Visualize菜单预览合成效果,再微调高宽值,最终让所有图片都能刚刚显示为好。

     

    导出格式格式设置为png。(如果图片有模糊可把Bit depth设置为32位试试,瞎猜的,不一定有用)

    合成图预览如下:

    c)  点击Options/Save bitmap font as..菜单,选择位置后进行保存操作,最终会得到两个文件(ArtNum.fnt和ArtNum_0.png),如下图:

     

    字体名字可以自由定义,导出的时候,每个方格要处在选中状态(浅灰色)。

    关于BMFont64软件的操作,也可以参考文章:Unity教程之-UGUI美术字体的制作与使用

    3、生成字体

    a)将上述两个文件导入到Unity中,在资源面板中鼠标右键,选择Create/MaterialCreate/Custom Font菜单,

    创建一个空的材质ArtNum_mat和一个空的自定义字体ArtNum(后缀为.fontsettings,在Unity中不显示),如下图:

     

    b) 点击Tools/BMFont Maker菜单,在打开的窗口中,选择相应的文件进行赋值,如下图,

    最后点击Create BMFont按钮,这样一个美术字体就生成了。

    点击字体文件,能在Inspector面板的Character Rects中看到字体的映射信息。

    c) 创建一个Text,输入一些数字字母和符号,字体选择为ArtNum,颜色选为白色,就能看到实际的效果。

      

    美术字体制作完成。

    后记

    使用这种字体的一些小问题
    1、字体不会换行,超出宽度的字体将会重叠显示,需要预留出宽度。

    2、字体不受Font Size的影响,无法动态调整大小,如有需要,可通过设置Scale来解决。

    3、如遇到字体信息在重启Unity后丢失的情况,可在BMFontEditor.cs脚本中的最后添加 EditorUtility.SetDirty(targetFont);来解决。

  • 相关阅读:
    Android应用性能测试之CPU和内存占用
    每天一个linux命令(30): chown命令
    安装apk时出现错误Failure [INSTALL_FAILED_DEXOPT]问题解决的方法
    android adb shell 命令大全
    adb logcat命令查看并过滤android输出log
    Ubuntu里字符编码设置
    linux下GBK->UTF-8文件编码批量转换脚本
    Android开发之如何保证Service不被杀掉(broadcast+system/app)
    android的m、mm、mmm编译命令的使用
    解决 samba不允许一个用户使用一个以上用户名与一个服务器或共享资源的多重连接
  • 原文地址:https://www.cnblogs.com/imteach/p/10743725.html
Copyright © 2020-2023  润新知