遊戲開發中常常遇到須要以美術字(而非字庫)作數字顯示的狀況,一般美術會提供一組包含單個數字(也會有其它字符)的圖片,多是一張整圖,也多是每一個數字分開的散圖。html
在此我以一張整圖這種狀況爲例,來講明美術字體的具體制做流程。整圖以下:express
整個製做過程須要用到三樣工具:app
- 字體數據製做工具
- 圖片切割工具
- 字體生成工具
一、字體數據製做工具
字體數據製做工具名爲BMFont,是一個Windows上的可執行軟件,下載網址爲:http://www.angelcode.com/products/bmfont/less
這裏選擇下載64位運行版(單體文件,無需安裝)編輯器
可也以點這裏下載:BMFont64.exeide
二、圖片切割工具
圖片切割工具是Unity中運行的一個工具類,類名爲ImageSlicer,放在Editor目錄下便可,代碼以下:工具
ImageSlicer.cs1 /** 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, width: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 }編譯完成後會在Assets菜單下生成一個ImageSlicer/Process to Sprites的菜單項,選中圖片而後右鍵也能夠看到。post
三、字體生成工具
字體生成工具也是Unity3d中一個第三方插件,名字也是BMFont(不知道和第一個軟件有什麼關聯)。本來是NGUI中的一個字體制做工具,現被大佬剝離出來,在UGUI中也能夠使用。字體
下載地址:BMFont字體生成工具ui
解壓到Assets目錄下便可,編譯完成後,會在Unity編輯器上生成一個Tools/BMFont Maker菜單。
BMFont插件是源碼形式的,共包括6個文件:
1.BMFont.cs//---------------------------------------------- // 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); } } } }
2.BMFontEditor.csusing 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(); } } }
3.BetterList.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 }
4.BMFontReader.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.\n" + "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 + "):\n" + 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 + "):\n" + 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 + "):\n" + 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", ""); } } } } } }
5.BMGlyph.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; } }
6.ByteReader.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 != '\n' && ch != '\r') 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", "\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", "\n"); line += "\n" + s; } else { line = ReadLine(true); if (line == null) return null; line = line.Replace("\\n", "\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; } }
一、切割圖片
在字體數據製做軟件BMFont64中,須要使用單個數字的圖片,而我這個是一張包含全部數字和字母符號的整圖,就須要切成單張散圖。
a) 把圖片導入Unity,Sprite Mode選擇Multiple模式,勾選Read/Write Enable選項。見下圖:
而後點擊Sprite Editor進行多圖區域編輯,以下圖。能夠先按給定的三種方式進行劃分,本身再作細微調整。注意每一個字符邊距不要太大,否則作成字體後顯示起來就會很離散。
分割完成後,點擊Apply保存操做。
b) 選中圖片右鍵,執行ImageSlicer/Process to Sprites菜單,會生成一個與圖片同名的目錄,裏邊放着切割好的散圖。見下圖,
二、製做字體數據
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美術字體的製做與使用
三、生成字體
a)將上述兩個文件導入到Unity中,在資源面板中鼠標右鍵,選擇Create/Material和Create/Custom Font菜單,
建立一個空的材質ArtNum_mat和一個空的自定義字體ArtNum(後綴爲.fontsettings,在Unity中不顯示),以下圖:
b) 點擊Tools/BMFont Maker菜單,在打開的窗口中,選擇相應的文件進行賦值,以下圖,
最後點擊Create BMFont按鈕,這樣一個美術字體就生成了。
點擊字體文件,能在Inspector面板的Character Rects中看到字體的映射信息。
c) 建立一個Text,輸入一些數字字母和符號,字體選擇爲ArtNum,顏色選爲白色,就能看到實際的效果。
美術字體制做完成。
使用這種字體的一些小問題
一、字體不會換行,超出寬度的字體將會重疊顯示,須要預留出寬度。二、字體不受Font Size的影響,沒法動態調整大小,若有須要,可經過設置Scale來解決。
三、如遇到字體信息在重啓Unity後丟失的狀況,可在BMFontEditor.cs腳本中的最後添加 EditorUtility.SetDirty(targetFont);來解決。