這個僅僅是修改fnt,不負責從fnt生成unity字型檔案。
fnt生成字型檔案看這邊:ugui點陣圖字型使用 - fnt生成fontsettings工具
效果
工具程式碼
public class FntEditTool : EditorWindow { [MenuItem("MyTools/Fnt Edit Tool", false, 100)] private static void ShowWindow() { var wnd = GetWindow<FntEditTool>(false, "FntEditTool"); } private int m_ToolbarIndex = 0; private string[] m_ToolbarNames = { "Chars", "Kernings" }; private Vector2 m_ScrollPos = new Vector2(0, 0); private GUIContent m_TempLabelContent = new GUIContent(); private string m_FntFilePath = ""; private Fnt m_Fnt; private Texture2D m_FntTex; private bool m_IsFntTexHeightNoMax = false; private bool m_ShowSpriteFrame = false; private void OnGUI() { GUILayout.Space(6); EditorGUI.BeginChangeCheck(); UnityEngine.Object obj = null; GUI_ObjectField("Fnt檔案:", ref obj, typeof(TextAsset)); GUILayout.Space(2); if (EditorGUI.EndChangeCheck()) { if (null != obj) m_FntFilePath = AssetDatabase.GetAssetPath(obj); } GUI_TextField("Fnt檔案路徑:", ref m_FntFilePath); GUILayout.Space(2); if (string.IsNullOrEmpty(m_FntFilePath) || !File.Exists(m_FntFilePath)) { EditorGUILayout.HelpBox($"請先選擇Fnt檔案", MessageType.Warning); GUILayout.Space(2); GUI.enabled = false; } EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("讀取Fnt檔案")) { m_Fnt = new FntParser().Parse(m_FntFilePath); string fntFileFolderPath = Path.GetDirectoryName(m_FntFilePath); var fntTexPath = Path.Combine(fntFileFolderPath, m_Fnt.file); if (File.Exists(fntTexPath)) m_FntTex = AssetDatabase.LoadAssetAtPath<Texture2D>(fntTexPath); else m_FntTex = null; } if (GUILayout.Button("儲存Fnt")) { SaveFntFile(m_FntFilePath, m_Fnt); } EditorGUILayout.EndHorizontal(); GUI.enabled = true; if (null != m_Fnt) { bool isScrollViewShow = false; OnGUI_FntTex(ref isScrollViewShow); GUILayout.Space(6); if (!isScrollViewShow) m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos); EditorGUILayout.BeginHorizontal(); { GUI_IntField("FontSize:", ref m_Fnt.fontSize); GUI_IntField("LineHeight:", ref m_Fnt.lineHeight); GUI_IntField("BaseLine:", ref m_Fnt.baseLine); } EditorGUILayout.EndHorizontal(); EditorGUILayout.LabelField($"texWidth: {m_Fnt.texWidth}, texHeight: {m_Fnt.texHeight}"); m_ToolbarIndex = GUILayout.Toolbar(m_ToolbarIndex, m_ToolbarNames); switch (m_ToolbarIndex) { case 0: OnGUI_Chars(); break; case 1: OnGUI_Kernings(); break; } EditorGUILayout.EndScrollView(); } } private void GUI_ObjectField(string prefix, ref UnityEngine.Object v, Type t) { float bak = EditorGUIUtility.labelWidth; m_TempLabelContent.text = prefix; EditorGUIUtility.labelWidth = GUI.skin.label.CalcSize(m_TempLabelContent).x + 2; v = EditorGUILayout.ObjectField(m_TempLabelContent, v, t, false); EditorGUIUtility.labelWidth = bak; } private void GUI_TextField(string prefix, ref string v, int editBoxMaxWidth = -1) { float bak = EditorGUIUtility.labelWidth; m_TempLabelContent.text = prefix; EditorGUIUtility.labelWidth = GUI.skin.label.CalcSize(m_TempLabelContent).x + 2; if (editBoxMaxWidth > 0) v = EditorGUILayout.TextField(m_TempLabelContent, v, GUILayout.MaxWidth(EditorGUIUtility.labelWidth + editBoxMaxWidth)); else v = EditorGUILayout.TextField(m_TempLabelContent, v); EditorGUIUtility.labelWidth = bak; } private void GUI_IntField(string prefix, ref int v, int editBoxMaxWidth = -1) { float bak = EditorGUIUtility.labelWidth; m_TempLabelContent.text = prefix; EditorGUIUtility.labelWidth = GUI.skin.label.CalcSize(m_TempLabelContent).x + 2; if (editBoxMaxWidth > 0) v = EditorGUILayout.IntField(m_TempLabelContent, v, GUILayout.MaxWidth(EditorGUIUtility.labelWidth + editBoxMaxWidth)); else v = EditorGUILayout.IntField(m_TempLabelContent, v); EditorGUIUtility.labelWidth = bak; } private void OnGUI_FntTex(ref bool isScrollViewShow) { if (null != m_FntTex) { m_IsFntTexHeightNoMax = EditorGUILayout.ToggleLeft("貼圖預覽無高度限制", m_IsFntTexHeightNoMax); float fntTexMaxHeight = 0; if (m_IsFntTexHeightNoMax) { m_ScrollPos = EditorGUILayout.BeginScrollView(m_ScrollPos); isScrollViewShow = true; GUILayout.Label(m_FntTex); } else { fntTexMaxHeight = 150; GUILayout.Label(m_FntTex, GUILayout.MaxHeight(fntTexMaxHeight)); } var lastRect = GUILayoutUtility.GetLastRect(); float scale = 1; if (fntTexMaxHeight > 0) { scale = fntTexMaxHeight / m_FntTex.height; if (scale > 1) scale = 1; } m_ShowSpriteFrame = EditorGUILayout.ToggleLeft("字元Sprite外框", m_ShowSpriteFrame); string focusCtrlName = GUI.GetNameOfFocusedControl(); if (string.IsNullOrEmpty(focusCtrlName)) { foreach (var ch in m_Fnt.charList) { DrawCharFrame(scale, lastRect, ch); } } else { if (int.TryParse(focusCtrlName, out var num)) { int charIndex = num % 100; if (charIndex < m_Fnt.charList.Count) { DrawCharFrame(scale, lastRect, m_Fnt.charList[charIndex]); } } } } } private void DrawCharFrame(float scale, Rect lastRect, FntChar ch) { if (ch.w > 0 && ch.h > 0) { Vector2 leftTop = lastRect.min; if (m_ShowSpriteFrame) { //字元Sprite框 Vector2 leftTop1 = new Vector2(leftTop.x + ch.x * scale, leftTop.y + ch.y * scale); Vector2 rightBottom1 = new Vector2(leftTop1.x + ch.w * scale, leftTop1.y + ch.h * scale); DrawRect(leftTop1, rightBottom1); } //排版框 Handles.color = Color.red; Vector2 leftTop2 = new Vector2(leftTop.x + (ch.x - ch.xoff) * scale, leftTop.y + (ch.y - ch.yoff) * scale); Vector2 rightBottom2 = new Vector2(leftTop2.x + ch.xadv * scale, leftTop2.y + m_Fnt.lineHeight * scale); DrawRect(leftTop2, rightBottom2); Handles.color = Color.white; } } private void DrawRect(Vector2 leftTop, Vector2 rightBottom) { Vector2 leftBottom = new Vector2(leftTop.x, rightBottom.y); Vector2 rightTop = new Vector2(rightBottom.x, leftTop.y); Handles.DrawAAPolyLine(2f, leftTop, rightTop, rightBottom, leftBottom, leftTop); } private void OnGUI_Chars() { GUILayout.Space(6); //EditorGUILayout.LabelField($"Chars: {GUI.GetNameOfFocusedControl()}, {EditorGUIUtility.hotControl}"); for (int i = 0; i < m_Fnt.charList.Count; ++i) { int controlType = 100; var fntChar = m_Fnt.charList[i]; EditorGUILayout.BeginVertical("box"); { EditorGUILayout.BeginHorizontal(); GUI.SetNextControlName($"{controlType + i}"); controlType += 100; fntChar.ch = EditorGUILayout.TextField("char:", fntChar.ch, GUILayout.Width(120)); if (string.IsNullOrEmpty(fntChar.ch)) { fntChar.id = 0; } else { GUILayout.Space(10); int charCode2 = char.ConvertToUtf32(fntChar.ch, 0); fntChar.id = charCode2; EditorGUILayout.LabelField($"CharCode: {charCode2.ToString()}"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.LabelField($"精靈在貼圖上的資訊:"); EditorGUILayout.LabelField($"xy: ({fntChar.x}, {fntChar.y}) size: ({fntChar.w}, {fntChar.h})"); EditorGUILayout.LabelField("排版資訊:"); EditorGUILayout.BeginHorizontal(); GUI.SetNextControlName($"{controlType + i}"); controlType += 100; GUI_IntField("xoff:", ref fntChar.xoff); GUI.SetNextControlName($"{controlType + i}"); controlType += 100; GUI_IntField("yoff:", ref fntChar.yoff); GUI.SetNextControlName($"{controlType + i}"); controlType += 100; GUI_IntField("xadv:", ref fntChar.xadv); EditorGUILayout.EndHorizontal(); GUILayout.Space(5); if (GUILayout.Button("X", GUILayout.Width(30))) { m_Fnt.charList.RemoveAt(i); i--; } } EditorGUILayout.EndVertical(); GUILayout.Space(10); //條目間隔 } } private void OnGUI_Kernings() { GUILayout.Space(6); if (GUILayout.Button("+")) { var kern = new FntKerning(); m_Fnt.kerningList.Add(kern); } for (int i = 0; i < m_Fnt.kerningList.Count; ++i) { var kern = m_Fnt.kerningList[i]; EditorGUILayout.BeginVertical("box"); { EditorGUILayout.BeginHorizontal(); GUI_TextField("字元1:", ref kern.ch1, 200); if (string.IsNullOrEmpty(kern.ch1)) { kern.first = 0; } else { GUILayout.Space(10); int charCode2 = char.ConvertToUtf32(kern.ch1, 0); kern.first = charCode2; EditorGUILayout.LabelField($"CharCode: {charCode2.ToString()}"); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); GUI_TextField("字元2:", ref kern.ch2, 200); if (string.IsNullOrEmpty(kern.ch2)) { kern.second = 0; } else { GUILayout.Space(10); int charCode2 = char.ConvertToUtf32(kern.ch2, 0); kern.second = charCode2; EditorGUILayout.LabelField($"CharCode: {charCode2.ToString()}"); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); GUI_IntField("間距修正:", ref kern.amount, 200); } GUILayout.Space(5); if (GUILayout.Button("X", GUILayout.Width(30))) { m_Fnt.kerningList.RemoveAt(i); i--; } } EditorGUILayout.EndVertical(); GUILayout.Space(10); //條目間隔 } private static void SaveFntFile(string fntFilePath, Fnt fnt) { if (!EditorUtility.DisplayDialog("提示", $"是否覆蓋{fntFilePath}?", "Yes", "No")) return; using (StreamWriter sw = new StreamWriter(fntFilePath, false, Encoding.UTF8)) { sw.WriteLine($"info face=\"Art\" size={fnt.fontSize}"); sw.WriteLine($"common lineHeight={fnt.lineHeight} base={fnt.baseLine} scaleW={fnt.texWidth} scaleH={fnt.texHeight} pages=1"); sw.WriteLine($"page id=0 file=\"{fnt.file}\""); sw.WriteLine($"chars count={fnt.charList.Count}"); foreach (var c in fnt.charList) { sw.WriteLine($"char id={c.id} x={c.x} y={c.y} width={c.w} height={c.h} xoffset={c.xoff} yoffset={c.yoff} xadvance={c.xadv} page=0"); } int kernCnt = fnt.kerningList.Count; if (kernCnt > 0) { sw.WriteLine($"kernings count={kernCnt}"); foreach (var k in fnt.kerningList) { sw.WriteLine($"kerning first={k.first} second={k.second} amount={k.amount}"); } } } AssetDatabase.Refresh(); } }
fnt檔案資訊
public class Fnt { public int fontSize = 22; //字型大小 public int lineHeight = 22; //行高 public int baseLine = 20; //基線位置 public int texWidth = 512; //貼圖寬度 public int texHeight = 256; //貼圖高度 public string file = ""; //貼圖名字 public List<FntChar> charList = new List<FntChar>(); public List<FntKerning> kerningList = new List<FntKerning>(); } public class FntChar { public string ch = ""; //對應字元 //unicode碼 public int id; //在貼圖上的座標(左上角), 相對貼圖左上角 public int x; public int y; //在貼圖上的寬高 public int w; public int h; //排版修正 public int xoff; public int yoff; //排版寬度 public int xadv; } public class FntKerning { public string ch1; //字元1 public int first; //字元1的unicode碼 public string ch2; //字元2 public int second; //字元2的unicode碼 public int amount; //x方向修正 }
fnt檔案解析
public class FntParser { private Fnt m_Fnt; private Regex m_KVRegexPattern; public Fnt Parse(string filePath) { if (null == m_KVRegexPattern) m_KVRegexPattern = new Regex(@"(\S+)=""?([\w-.]+)""?"); m_Fnt = new Fnt(); using (StreamReader sr = new StreamReader(filePath, Encoding.UTF8)) { string lineStr = ""; int lineNum = 0; while (!string.IsNullOrEmpty(lineStr = sr.ReadLine())) { lineNum++; if (lineStr.StartsWith("info ")) { ParseInfoLine(lineStr); } else if (lineStr.StartsWith("common ")) { ParseCommonLine(lineStr); } else if (lineStr.StartsWith("page ")) { ParsePageLine(lineStr); } else if (lineStr.StartsWith("chars ")) { } else if (lineStr.StartsWith("char ")) { ParseCharLine(lineStr); } else if (lineStr.StartsWith("kernings ")) { } else if (lineStr.StartsWith("kerning ")) { ParseKerningLine(lineStr); } } } return m_Fnt; } private void ParseInfoLine(string infoLine) { MatchCollection result = m_KVRegexPattern.Matches(infoLine); for (var i = 0; i < result.Count; ++i) { Match item = result[i]; if (3 == item.Groups.Count) { var key = item.Groups[1].Value; var value = item.Groups[2].Value; switch (key) { case "size": m_Fnt.fontSize = int.Parse(value); break; } } } } private void ParseCommonLine(string commonLine) { MatchCollection result = m_KVRegexPattern.Matches(commonLine); for (var i = 0; i < result.Count; ++i) { Match item = result[i]; if (3 == item.Groups.Count) { var key = item.Groups[1].Value; var value = item.Groups[2].Value; switch (key) { case "lineHeight": m_Fnt.lineHeight = int.Parse(value); break; case "base": m_Fnt.baseLine = int.Parse(value); break; case "scaleW": m_Fnt.texWidth = int.Parse(value); break; case "scaleH": m_Fnt.texHeight = int.Parse(value); break; } } } } private void ParsePageLine(string pageLine) { int id = -1; string file = ""; MatchCollection result = m_KVRegexPattern.Matches(pageLine); for (var i = 0; i < result.Count; ++i) { Match item = result[i]; if (3 == item.Groups.Count) { var key = item.Groups[1].Value; var value = item.Groups[2].Value; //Debug.Log($"page: key='{key}', value='{value}'"); switch (key) { case "id": id = int.Parse(value); break; case "file": file = value; break; } } } if (0 == id) m_Fnt.file = file; else Debug.LogWarning ($"page != 0, ignore: {pageLine}"); } private int ParseCharsOrKerningCount(string line) { MatchCollection result = m_KVRegexPattern.Matches(line); for (var i = 0; i < result.Count; ++i) { Match item = result[i]; if (3 == item.Groups.Count) { var key = item.Groups[1].Value; var value = item.Groups[2].Value; switch (key) { case "count": return int.Parse(value); } } } return 0; } private void ParseCharLine(string charLine) { FntChar fntChar = new FntChar(); MatchCollection result = m_KVRegexPattern.Matches(charLine); int page = 0; for (var i = 0; i < result.Count; ++i) { Match item = result[i]; if (3 == item.Groups.Count) { var key = item.Groups[1].Value; var value = item.Groups[2].Value; //Debug.Log($"char: key:'{key}', value:'{value}'"); switch (key) { case "id": fntChar.id = int.Parse(value); fntChar.ch = char.ConvertFromUtf32(fntChar.id); break; case "x": fntChar.x = int.Parse(value); break; case "y": fntChar.y = int.Parse(value); break; case "width": fntChar.w = int.Parse(value); break; case "height": fntChar.h = int.Parse(value); break; case "xoffset": fntChar.xoff = int.Parse(value); break; case "yoffset": fntChar.yoff = int.Parse(value); break; case "xadvance": fntChar.xadv = int.Parse(value); break; case "page": page = int.Parse(value); break; } } } if (0 == page) m_Fnt.charList.Add(fntChar); else Debug.LogWarning($"page != 0, ignore:{charLine}"); } private void ParseKerningLine(string kerningLine) { MatchCollection result = m_KVRegexPattern.Matches(kerningLine); var fntKern = new FntKerning(); for (var i = 0; i < result.Count; ++i) { Match item = result[i]; if (3 == item.Groups.Count) { var key = item.Groups[1].Value; var value = item.Groups[2].Value; //Debug.Log($"kerning: key:'{key}', value:'{value}'"); switch (key) { case "first": fntKern.first = int.Parse(value); fntKern.ch1 = char.ConvertFromUtf32(fntKern.first); break; case "second": fntKern.second = int.Parse(value); fntKern.ch2 = char.ConvertFromUtf32(fntKern.second); break; case "amount": fntKern.amount = int.Parse(value); break; } } } m_Fnt.kerningList.Add(fntKern); } }