1. 程式人生 > >Unity中製作自定義字型的兩種方式

Unity中製作自定義字型的兩種方式

Unity支援自定義圖片字型(CustomFont),網上有很多教程,細節不盡相同,當概括起來基本就是兩種方式。一是使用BMFont,匯出圖集和.fnt檔案,再使用圖集在Unity中設定得到字型。二是不用BMFont,使用Unity自帶的Sprite類似圖集的功能。兩種方式原理相同,只是手段有區別。基本原理都是先有一張貼圖,比如:

需要知道的資訊是貼圖中每一個字元對應的ASCII碼(例如0的ASCII碼為48)與該字元在圖集中對應的位置(0為x:0;y:0;w:55;h:76)。然後在Unity中建立材質和CustomFont並根據資訊進行設定。

最後得到字型。

兩種方式的區別僅在於第一步中如何得到圖集的資訊。具體的:

對於第一種使用BMFont的方式,目的是得到.fnt檔案,實際上是xml格式檔案。具體的資訊為:


BMFont的使用方法不再詳述。得到圖集個fnt檔案後,網上一般的方法是手動計算在Unity中的引數,有些繁瑣,在這裡寫一個Editor指令碼來自動完成這個過程。直接上程式碼:

using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using UnityEditor;
using UnityEngine;

public class CreateFontFromFnt : EditorWindow
{
    [MenuItem("Tools/建立字型(Fnt)")]
    static void DoIt()
    {
        GetWindow<CreateFontFromFnt>("建立字型");
    }
    private string fontName;
    private string fontPath;
    private Texture2D tex;
    private string fntFilePath;

    private void OnGUI()
    {
        GUILayout.BeginVertical();

        GUILayout.BeginHorizontal();
        GUILayout.Label("字型名稱:");
        fontName = EditorGUILayout.TextField(fontName);
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal();
        GUILayout.Label("字型圖片:");
        tex = (Texture2D)EditorGUILayout.ObjectField(tex, typeof(Texture2D), true);
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal();
        if (GUILayout.Button(string.IsNullOrEmpty(fontPath) ? "選擇路徑" : fontPath))
        {
            fontPath = EditorUtility.OpenFolderPanel("字型路徑", Application.dataPath, "");
            if (string.IsNullOrEmpty(fontPath))
            {
                Debug.Log("取消選擇路徑");
            }
            else
            {
                fontPath = fontPath.Replace(Application.dataPath, "") + "/";
            }
        }
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal();
        if (GUILayout.Button(string.IsNullOrEmpty(fntFilePath) ? "選擇fnt檔案" : fntFilePath))
        {
            fntFilePath = EditorUtility.OpenFilePanelWithFilters("選擇fnt檔案", Environment.GetFolderPath(Environment.SpecialFolder.Desktop), new string[] { "", "fnt" });
            if (string.IsNullOrEmpty(fntFilePath))
            {
                Debug.Log("取消選擇路徑");
            }
        }
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal();
        if (GUILayout.Button("建立"))
        {
            Create();
        }
        GUILayout.EndHorizontal();

        GUILayout.EndVertical();
    }

    private void Create()
    {
        if (string.IsNullOrEmpty(fntFilePath))
        {
            Debug.LogError("fnt為空");
            return;
        }
        if (tex == null)
        {
            Debug.LogError("字型圖片為空");
            return;
        }

        string fontSettingPath = fontPath + fontName + ".fontsettings";
        string matPath = fontPath + fontName + ".mat";
        if (File.Exists(Application.dataPath + fontSettingPath))
        {
            Debug.LogErrorFormat("已存在同名字型檔案:{0}", fontSettingPath);
            return;
        }
        if (File.Exists(Application.dataPath + matPath))
        {
            Debug.LogErrorFormat("已存在同名字型材質:{0}", matPath);
            return;
        }
        var list = new List<CharacterInfo>();
        XmlDocument xmlDoc = new XmlDocument();
        var content = File.ReadAllText(fntFilePath, System.Text.Encoding.UTF8);
        xmlDoc.LoadXml(content);
        var nodelist = xmlDoc.SelectNodes("font/chars/char");
        foreach (XmlElement item in nodelist)
        {
            CharacterInfo info = new CharacterInfo();
            var id = int.Parse(item.GetAttribute("id"));
            var x = float.Parse(item.GetAttribute("x"));
            var y = float.Parse(item.GetAttribute("y"));
            var width = float.Parse(item.GetAttribute("width"));
            var height = float.Parse(item.GetAttribute("height"));

            info.index = id;
            //紋理對映,上下翻轉
            info.uvBottomLeft = new Vector2(x / tex.width, 1 - (y + height) / tex.height);
            info.uvBottomRight = new Vector2((x + width) / tex.width, 1 - (y + height) / tex.height);
            info.uvTopLeft = new Vector2(x / tex.width, 1 - y / tex.height);
            info.uvTopRight = new Vector2((x + width) / tex.width, 1 - y / tex.height);

            info.minX = 0;
            info.maxX = (int)width;
            info.minY = -(int)height / 2;
            info.maxY = (int)height / 2;
            info.advance = (int)width;

            list.Add(info);
        }

        Material mat = new Material(Shader.Find("GUI/Text Shader"));
        mat.SetTexture("_MainTex", tex);
        Font m_myFont = new Font();
        m_myFont.material = mat;
        AssetDatabase.CreateAsset(mat, "Assets" + matPath);
        AssetDatabase.CreateAsset(m_myFont, "Assets" + fontSettingPath);
        m_myFont.characterInfo = list.ToArray();
        EditorUtility.SetDirty(m_myFont);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        Debug.Log("建立成功!");
    }
}

使用起來很簡單:


程式碼也沒什麼可深究的,目的是代替手動計算,只是在紋理對映的時候有一個小坑。

第二種方式使用Unity中的Sprite。Unity支援把一個Sprite切割成多個。可以用這種方式代替BMFont匯出的fnt檔案。需要手動做的工作是將圖集的TextureType設定為Sprite,然後把SpriteMode設為Multiple,開啟SpriteEditor,對圖片進行切割。Slice就基本可以完成這個工作,如果需要再手動微調一下。


一張圖按照字元的位置分割成了10個Sprite。然後選中每一個Sprite把Name設定成字元對應的ASCII碼。這樣第一種方法裡fnt檔案包含的資訊基本都包含在這個“圖集”裡了。同樣寫一個Editor指令碼把這些資訊寫入到CustomFont裡面,並不用手動去建立。

using UnityEngine;
using UnityEditor;
using System.IO;

public class CreateFont : EditorWindow
{
    [MenuItem("Tools/建立字型(sprite)")]
    public static void Open()
    {
        GetWindow<CreateFont>("建立字型");
    }

    private Texture2D tex;
    private string fontName;
    private string fontPath;

    private void OnGUI()
    {
        GUILayout.BeginVertical();

        GUILayout.BeginHorizontal();
        GUILayout.Label("字型圖片:");
        tex = (Texture2D)EditorGUILayout.ObjectField(tex, typeof(Texture2D), true);
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal();
        GUILayout.Label("字型名稱:");
        fontName = EditorGUILayout.TextField(fontName);
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal();
        if (GUILayout.Button(string.IsNullOrEmpty(fontPath) ? "選擇路徑" : fontPath))
        {
            fontPath = EditorUtility.OpenFolderPanel("字型路徑", Application.dataPath, "");
            if (string.IsNullOrEmpty(fontPath))
            {
                Debug.Log("取消選擇路徑");
            }
            else
            {
                fontPath = fontPath.Replace(Application.dataPath, "") + "/";
            }
        }
        GUILayout.EndHorizontal();

        GUILayout.BeginHorizontal();
        if (GUILayout.Button("建立"))
        {
            Create();
        }
        GUILayout.EndHorizontal();

        GUILayout.EndVertical();
    }

    private void Create()
    {
        if (tex == null)
        {
            Debug.LogWarning("建立失敗,圖片為空!");
            return;
        }

        if (string.IsNullOrEmpty(fontPath))
        {
            Debug.LogWarning("字型路徑為空!");
            return;
        }
        if (fontName == null)
        {
            Debug.LogWarning("建立失敗,字型名稱為空!");
            return;
        }
        else
        {
            if (File.Exists(Application.dataPath + fontPath + fontName + ".fontsettings"))
            {
                Debug.LogError("建立失敗,已存在同名字型檔案");
                return;
            }
            if (File.Exists(Application.dataPath + fontPath + fontName + ".mat"))
            {
                Debug.LogError("建立失敗,已存在同名字型材質檔案");
                return;
            }
        }

        string selectionPath = AssetDatabase.GetAssetPath(tex);
        if (selectionPath.Contains("/Resources/"))
        {
            string selectionExt = Path.GetExtension(selectionPath);
            if (selectionExt.Length == 0)
            {
                Debug.LogError("建立失敗!");
                return;
            }
            
            string fontPathName = fontPath + fontName + ".fontsettings";
            string matPathName = fontPath + fontName + ".mat";
            float lineSpace = 0.1f;
            //string loadPath = selectionPath.Remove(selectionPath.Length - selectionExt.Length).Replace("Assets/Resources/", "");
            string loadPath = selectionPath.Replace(selectionExt, "").Substring(selectionPath.IndexOf("/Resources/") + "/Resources/".Length);
            Sprite[] sprites = Resources.LoadAll<Sprite>(loadPath);
            if (sprites.Length > 0)
            {
                Material mat = new Material(Shader.Find("GUI/Text Shader"));
                mat.SetTexture("_MainTex", tex);
                Font m_myFont = new Font();
                m_myFont.material = mat;
                CharacterInfo[] characterInfo = new CharacterInfo[sprites.Length];
                for (int i = 0; i < sprites.Length; i++)
                {
                    if (sprites[i].rect.height > lineSpace)
                    {
                        lineSpace = sprites[i].rect.height;
                    }
                }
                for (int i = 0; i < sprites.Length; i++)
                {
                    Sprite spr = sprites[i];
                    CharacterInfo info = new CharacterInfo();
                    try
                    {
                        info.index = System.Convert.ToInt32(spr.name);
                    }
                    catch
                    {
                        Debug.LogError("建立失敗,Sprite名稱錯誤!");
                        return;
                    }
                    Rect rect = spr.rect;
                    float pivot = spr.pivot.y / rect.height - 0.5f;
                    if (pivot > 0)
                    {
                        pivot = -lineSpace / 2 - spr.pivot.y;
                    }
                    else if (pivot < 0)
                    {
                        pivot = -lineSpace / 2 + rect.height - spr.pivot.y;
                    }
                    else
                    {
                        pivot = -lineSpace / 2;
                    }
                    int offsetY = (int)(pivot + (lineSpace - rect.height) / 2);
                    info.uvBottomLeft = new Vector2((float)rect.x / tex.width, (float)(rect.y) / tex.height);
                    info.uvBottomRight = new Vector2((float)(rect.x + rect.width) / tex.width, (float)(rect.y) / tex.height);
                    info.uvTopLeft = new Vector2((float)rect.x / tex.width, (float)(rect.y + rect.height) / tex.height);
                    info.uvTopRight = new Vector2((float)(rect.x + rect.width) / tex.width, (float)(rect.y + rect.height) / tex.height);
                    info.minX = 0;
                    info.minY = -(int)rect.height - offsetY;
                    info.maxX = (int)rect.width;
                    info.maxY = -offsetY;
                    info.advance = (int)rect.width;
                    characterInfo[i] = info;
                }
                AssetDatabase.CreateAsset(mat, "Assets" + matPathName);
                AssetDatabase.CreateAsset(m_myFont, "Assets" + fontPathName);
                m_myFont.characterInfo = characterInfo;
                EditorUtility.SetDirty(m_myFont);
                AssetDatabase.SaveAssets();
                AssetDatabase.Refresh();//重新整理資源
                Debug.Log("建立字型成功");

            }
            else
            {
                Debug.LogError("圖集錯誤!");
            }
        }
        else
        {
            Debug.LogError("建立失敗,選擇的圖片不在Resources資料夾內!");
        }
    }
}

這個指令碼參考了某一篇博文,時間長了實在是找不到了。

原理跟第一種方法一樣,只是計算細節略有差異。使用起來還是很簡單:


大同小異的兩種方法,個人更喜歡用第二種。不需要使用額外的軟體,一鍵搞定,基本上可以丟給美術童鞋來做了~~~