对网上大佬的代码进行了易用性修改,仅WINDOWS可用。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;
public class BmfInfo
{
public string filePath;
public string fileName;
public int outWidth = 256;
public int outHeight = 256;
}
public class BMFontTools : EditorWindow
{
private string configDirPath;
private string configFlag = "# imported icon images";
//字体文件生成路径
private string fontDirPath;
private Vector2 scrollPos;
private string currentSelect = string.Empty;
private struct FontInfo
{
//图片路径
public string ImgPath;
//字符
public char charInfo;
}
private List<FontInfo> _fontInfoList = new List<FontInfo>();
private readonly List<BmfInfo> bmfInfoList = new List<BmfInfo>();
private int xAdvanceOffset;
private void Awake()
{
configDirPath = Path.Combine(Application.dataPath, "../Tools/BMFont");
fontDirPath = Path.Combine(Application.dataPath, "AllRes/Digit");
InitConfigInfo();
}
private void InitConfigInfo()
{
bmfInfoList.Clear();
List<string> pathList = new List<string>(Directory.GetFiles(configDirPath, "*.bmfc"));
for (int i = 0; i < pathList.Count; i++)
{
BmfInfo info = new BmfInfo();
info.fileName = Path.GetFileNameWithoutExtension(pathList[i]);
info.filePath = pathList[i];
string[] tempLines = File.ReadAllLines(info.filePath);
foreach (string tempLine in tempLines)
{
if (tempLine.StartsWith("outWidth="))
{
string infoTemp = tempLine.Split('#')[0];
int width = int.Parse(infoTemp.Replace("outWidth=", string.Empty));
info.outWidth = width;
}
else if (tempLine.StartsWith("outHeight="))
{
string infoTemp = tempLine.Split('#')[0];
int height = int.Parse(infoTemp.Replace("outHeight=", string.Empty));
info.outHeight = height;
}
}
bmfInfoList.Add(info);
}
if (!IsNullOrEmpty(bmfInfoList))
{
currentSelect = bmfInfoList[0].filePath;
_fontInfoList = AnalysisConfig(currentSelect);
}
}
#if UNITY_EDITOR_WIN
[MenuItem("AssetsTools/BMFont/BMFontTextureTools", false)]
private static void MyBMFontTools()
{
string path = Path.Combine(Application.dataPath, "../Tools/BMFont/bmfont.exe");
if (!File.Exists(path))
{
Debug.LogWarning($"{path}目录下不存在[bmfont.exe]");
if (EditorUtility.DisplayDialog("BMFont工具不存在", $"{path}目录下不存在[bmfont.exe]", "这就去下载"))
{
Application.OpenURL("http://www.angelcode.com/products/bmfont/");
};
return;
}
path = Path.Combine(Application.dataPath, "../Tools/BMFont/BMFontGenerate.bat");
if (!File.Exists(path))
{
File.WriteAllText(path, "bmfont.exe -c %1 -o %2 ");
}
BMFontTools bmFont = GetWindow<BMFontTools>();
bmFont.Show();
}
#endif
private void OnGUI()
{
GUILayout.Label("使用说明:");
GUILayout.Label("1、点击NewFont创建新的字体配置。");
GUILayout.Label("2、修改FontName后,点击ReName修改字体名字。");
GUILayout.Label("3、outWidth和outHeight为输出图集宽高,如果输出多张图集,请增大图集宽高。");
GUILayout.Label("4、点击Select选中字体配置,选中后为绿色。");
GUILayout.Label("5、点击下方Add按钮,增加字符。");
GUILayout.Label("6、点击SelectImg选择图片,要求路径是全英文路径。");
GUILayout.Label("7、[可选]修改字符的X轴间隔。");
GUILayout.Label("8、点击下方SaveAndExportFont输出字体文件。");
GUILayout.Label("9、如果生成失败,请检查图片格式是否为PNG-24,即位深度为32。");
GUILayout.BeginHorizontal();
GUILayout.FlexibleSpace();
if (GUILayout.Button("Refresh", EditorStyles.toolbarButton))
{
InitConfigInfo();
}
if (GUILayout.Button("NewFont", EditorStyles.toolbarButton))
{
string fileName = $"{DateTime.Now:yyyyMMddhhmmss}.bmfc";
File.WriteAllText(Path.Combine(configDirPath, fileName), configFlag, Encoding.UTF8);
InitConfigInfo();
currentSelect = ToFind(bmfInfoList, x => x.filePath.Contains(fileName)).filePath;
_fontInfoList = AnalysisConfig(currentSelect);
}
if (GUILayout.Button("Delete", EditorStyles.toolbarButton))
{
if (File.Exists(currentSelect))
{
File.Delete(currentSelect);
}
_fontInfoList = new List<FontInfo>();
InitConfigInfo();
return;
}
GUILayout.EndHorizontal();
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
EditorGUILayout.BeginVertical();
EditorGUILayout.BeginHorizontal();
GUILayout.Label("Font Save Path:", GUILayout.MaxWidth(100));
fontDirPath = GUILayout.TextField(fontDirPath);
if (GUILayout.Button("SelectFold"))
{
fontDirPath = EditorUtility.OpenFolderPanel("SelectFontOutPutDir", "", "");
}
EditorGUILayout.EndHorizontal();
SetBMFontConfigs();
SetConfigInfo();
EditorGUILayout.EndVertical();
EditorGUILayout.EndScrollView();
}
private void SetBMFontConfigs()
{
for (int i = 0; i < bmfInfoList.Count; i++)
{
if (bmfInfoList[i].filePath.Equals(currentSelect))
{
GUI.color = Color.green;
}
string filePath = bmfInfoList[i].filePath;
GUILayout.BeginHorizontal();
GUI.enabled = false;
EditorGUILayout.TextField(filePath, GUILayout.MaxWidth(500));
GUI.enabled = true;
EditorGUILayout.LabelField("FontName:", GUILayout.MaxWidth(80));
bmfInfoList[i].fileName = EditorGUILayout.TextField(bmfInfoList[i].fileName, GUILayout.MaxWidth(100));
EditorGUILayout.LabelField("outWidth:", GUILayout.MaxWidth(70));
bmfInfoList[i].outWidth =
int.Parse(EditorGUILayout.TextField(bmfInfoList[i].outWidth.ToString(), GUILayout.MaxWidth(50)));
EditorGUILayout.LabelField("outHeight:", GUILayout.MaxWidth(70));
bmfInfoList[i].outHeight =
int.Parse(EditorGUILayout.TextField(bmfInfoList[i].outHeight.ToString(), GUILayout.MaxWidth(50)));
if (GUILayout.Button("ReName"))
{
Regex regex = new Regex(@"/|\\|<|>|\*|\?");
if (!string.IsNullOrEmpty(bmfInfoList[i].fileName) && !regex.IsMatch(bmfInfoList[i].fileName))
{
string fileNameTemp = filePath.Replace(Path.GetFileNameWithoutExtension(filePath),
bmfInfoList[i].fileName);
if (File.Exists(fileNameTemp))
{
Debug.LogError("文件冲突,命名失败");
}
else
{
File.Move(filePath, fileNameTemp);
}
InitConfigInfo();
}
else
{
Debug.LogError("文件名非法或为空,命名失败");
}
}
if (GUILayout.Button("Select"))
{
currentSelect = filePath;
_fontInfoList = AnalysisConfig(currentSelect);
}
GUILayout.EndHorizontal();
if (filePath.Equals(currentSelect))
{
GUI.color = Color.white;
}
}
}
private void SetConfigInfo()
{
if (!string.IsNullOrEmpty(currentSelect))
{
for (int i = 0; i < _fontInfoList.Count; i++)
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Select Img", GUILayout.MaxWidth(100)))
{
string pathTemp = EditorUtility.OpenFilePanelWithFilters("选择图片", "", new[] { "Image", "png" });
string fileName = Path.GetFileName(pathTemp).Replace(".png", "");
if (!string.IsNullOrEmpty(pathTemp))
{
FontInfo fontInfo = new FontInfo();
fontInfo.charInfo = _fontInfoList[i].charInfo;
if (fontInfo.charInfo == '\0')
{
if (int.TryParse(fileName, out int rs))
{
fontInfo.charInfo = rs.ToString()[0];
}
else
{
fontInfo.charInfo = fileName[fileName.Length - 1];
}
}
fontInfo.ImgPath = FormatPath(pathTemp);
_fontInfoList[i] = fontInfo;
int result = 0;
if (int.TryParse(fileName, out result) || int.TryParse(fileName[fileName.Length - 1].ToString(), out result))
{
int i2 = result;
int index = i + 1;
while (i2 < 10)
{
i2++;
string path2 = pathTemp.Replace(result + ".png", i2 + ".png");
if (!string.IsNullOrEmpty(path2) && File.Exists(path2))
{
FontInfo fontInfo2 = new FontInfo();
fontInfo2.charInfo = i2.ToString()[0];
fontInfo2.ImgPath = FormatPath(path2);
while (true)
{
if (_fontInfoList.Count <= index)
{
_fontInfoList.Add(fontInfo2);
index++;
break;
}
if (string.IsNullOrEmpty(_fontInfoList[index].ImgPath))
{
_fontInfoList[index] = fontInfo2;
index++;
break;
}
else
{
index++;
}
}
}
else
{
break;
}
}
}
}
}
EditorGUILayout.LabelField("Char:", GUILayout.MaxWidth(55));
if (!string.IsNullOrEmpty(_fontInfoList[i].charInfo.ToString()))
{
FontInfo info = new FontInfo();
string temp =
EditorGUILayout.TextField(_fontInfoList[i].charInfo.ToString(), GUILayout.MaxWidth(30));
if (temp.Length == 1 && Regex.IsMatch(temp, "[\x20-\x7e]"))
{
info.charInfo = temp[0];
info.ImgPath = _fontInfoList[i].ImgPath;
_fontInfoList[i] = info;
}
}
EditorGUILayout.LabelField("ImgPath:", GUILayout.MaxWidth(55));
GUI.enabled = false;
EditorGUILayout.TextField(_fontInfoList[i].ImgPath);
GUI.enabled = true;
if (GUILayout.Button("Delete"))
{
_fontInfoList.RemoveAt(i);
i--;
}
EditorGUILayout.EndHorizontal();
}
EditorGUILayout.LabelField($"当前字符数量:{_fontInfoList.Count}");
GUILayout.Space(10);
EditorGUILayout.BeginHorizontal();
int.TryParse(EditorGUILayout.TextField("额外的文字间隔", xAdvanceOffset.ToString()), out xAdvanceOffset);
EditorGUILayout.EndHorizontal();
if (GUILayout.Button("Add"))
{
_fontInfoList.Add(new FontInfo());
}
GUI.enabled = !IsNullOrEmpty(_fontInfoList);
GUILayout.Space(50);
if (GUILayout.Button("Save And Export Font"))
{
SaveFontAndExport();
}
GUI.enabled = true;
}
}
private void SaveFontAndExport()
{
BmfInfo bmfInfo = ToFind(bmfInfoList, x => x.filePath.Equals(currentSelect));
string baseFontInfo = File.ReadAllText(configDirPath + "/BaseConfig.bmf");
baseFontInfo = baseFontInfo
.Replace("outWidth=", $"outWidth={bmfInfo.outWidth}#")
.Replace("outHeight=", $"outHeight={bmfInfo.outHeight}#");
baseFontInfo += "\n";
for (int i = 0; i < _fontInfoList.Count; i++)
{
if (string.IsNullOrEmpty(_fontInfoList[i].ImgPath) ||
string.IsNullOrEmpty(_fontInfoList[i].charInfo.ToString()))
{
continue;
}
string info = $"icon=\"{_fontInfoList[i].ImgPath}\",{(int)_fontInfoList[i].charInfo},0,0,0\n";
baseFontInfo += info;
}
File.WriteAllText(currentSelect, baseFontInfo);
ExportFontInfo();
AssetDatabase.Refresh();
}
private void ExportFontInfo()
{
string fileName = Path.GetFileNameWithoutExtension(currentSelect);
string targetDir = Path.Combine(fontDirPath, fileName);
if (!Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
Process process = new Process();
string batPath = Path.Combine(configDirPath, "BMFontGenerate.bat");
process.StartInfo.FileName = batPath;
process.StartInfo.WorkingDirectory = configDirPath;
process.StartInfo.Arguments =
string.Format("{0} {1}", currentSelect, Path.Combine(fontDirPath, targetDir + "/" + fileName));
process.Start();
process.WaitForExit();
AssetDatabase.Refresh();
GenFontInfo(targetDir, fileName);
}
private string GetAssetPath(string path)
{
string pathTemp = path.Replace("\\", "/");
pathTemp = pathTemp.Replace(Application.dataPath, "Assets");
return pathTemp;
}
private void GenFontInfo(string fontDirPath, string fileName)
{
string matPath = Path.Combine(fontDirPath, fileName + "_0.mat");
Material mat = AssetDatabase.LoadAssetAtPath<Material>(GetAssetPath(matPath));
if (mat == null)
{
mat = new Material(Shader.Find("UI/Default Font"));
AssetDatabase.CreateAsset(mat, GetAssetPath(matPath));
}
string texturePath = Path.Combine(fontDirPath, fileName + "_0.png");
Texture _fontTexture = AssetDatabase.LoadAssetAtPath<Texture>(GetAssetPath(texturePath));
mat = AssetDatabase.LoadAssetAtPath<Material>(GetAssetPath(matPath));
mat.SetTexture("_MainTex", _fontTexture);
string fontPath = Path.Combine(fontDirPath, fileName + ".fontsettings");
Font font = AssetDatabase.LoadAssetAtPath<Font>(GetAssetPath(fontPath));
if (font == null)
{
font = new Font();
AssetDatabase.CreateAsset(font, GetAssetPath(fontPath));
}
string fontConfigPath = Path.Combine(fontDirPath, fileName + ".fnt");
List<CharacterInfo> chars = GetFontInfo(fontConfigPath, _fontTexture);
font.material = mat;
SerializeFont(font, chars, 1);
//File.Delete(fontConfigPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
private List<CharacterInfo> GetFontInfo(string fontConfig, Texture texture)
{
XmlDocument xml = new XmlDocument();
xml.Load(fontConfig);
XmlNode info = xml.GetElementsByTagName("info")[0];
XmlNodeList chars = xml.GetElementsByTagName("chars")[0].ChildNodes;
CharacterInfo[] charInfos = new CharacterInfo[chars.Count];
for (int cnt = 0; cnt < chars.Count; cnt++)
{
XmlNode node = chars[cnt];
CharacterInfo charInfo = new CharacterInfo();
charInfo.index = ToInt(node, "id");
charInfo.advance = (int)ToFloat(node, "xadvance") + xAdvanceOffset;
Rect r = GetUV(node, (Texture2D)texture);
//这里注意下UV坐标系和从BMFont里得到的信息的坐标系是不一样的哦,前者左下角为(0,0),
//右上角为(1,1)。而后者则是左上角上角为(0,0),右下角为(图宽,图高)
charInfo.uvBottomLeft = new Vector2(r.xMin, r.yMin); //字符uv左下角坐标
charInfo.uvBottomRight = new Vector2(r.xMax, r.yMin); //字符uv右下角坐标
charInfo.uvTopLeft = new Vector2(r.xMin, r.yMax); //字符uv左上角坐标
charInfo.uvTopRight = new Vector2(r.xMax, r.yMax); //字符uv右上角坐标
//Debug.LogError($"charInfo.advance = {charInfo.advance}"); // uv1.x 字符的横轴占用空间
//Debug.LogError($"r.xMin = {r.xMin}"); // uv1.x uv起始点x坐标
//Debug.LogError($"r.yMin = {r.yMin}"); // uv1.y uv起始点y坐标
//Debug.LogError($"r.xMax = {r.xMax}"); // uv2.x uv终止点y坐标
//Debug.LogError($"r.yMax = {r.yMax}"); // uv2.y uv终止点y坐标
//Debug.Log("charInfo.minY = " + charInfo.minY); // 同上uv
//Debug.Log("charInfo.maxY = " + charInfo.maxY);
//Debug.Log("charInfo.minX = " + charInfo.minX);
//Debug.Log("charInfo.maxX = " + charInfo.maxX);
//Debug.Log("------------------------");
r = GetVert(node);
charInfo.minX = (int)r.xMin;
charInfo.maxX = (int)r.xMax;
//不居中
//charInfo.minY = (int)r.yMax;
//charInfo.maxY = (int)r.yMin;
//居中显示
charInfo.minY = (int)((r.yMax - r.yMin) / 2);
charInfo.maxY = -(int)((r.yMax - r.yMin) / 2);
charInfos[cnt] = charInfo;
}
return new List<CharacterInfo>(charInfos);
}
private static void SetLineHeight(SerializedObject font, float height)
{
font.FindProperty("m_LineSpacing").floatValue = height;
}
private static SerializedObject SerializeFont(Font font, List<CharacterInfo> chars, float lineHeight)
{
SerializedObject serializedFont = new SerializedObject(font);
SetLineHeight(serializedFont, lineHeight);
SerializeFontCharInfos(serializedFont, chars);
serializedFont.ApplyModifiedProperties();
return serializedFont;
}
private static void SerializeFontCharInfos(SerializedObject font, List<CharacterInfo> chars)
{
SerializedProperty charRects = font.FindProperty("m_CharacterRects");
charRects.arraySize = chars.Count;
for (int i = 0; i < chars.Count; ++i)
{
CharacterInfo info = chars[i];
SerializedProperty prop = charRects.GetArrayElementAtIndex(i);
SerializeCharInfo(prop, info);
}
}
private static void SerializeCharInfo(SerializedProperty prop, CharacterInfo charInfo)
{
prop.FindPropertyRelative("index").intValue = charInfo.index;
prop.FindPropertyRelative("uv").rectValue = charInfo.uv;
prop.FindPropertyRelative("vert").rectValue = charInfo.vert;
prop.FindPropertyRelative("advance").floatValue = charInfo.advance;
prop.FindPropertyRelative("flipped").boolValue = false;
}
private List<FontInfo> AnalysisConfig(string configPath)
{
List<FontInfo> infoList = new List<FontInfo>();
string[] fileInfo = File.ReadAllLines(configPath);
bool isGetInfoFlag = false;
for (int i = 0; i < fileInfo.Length; i++)
{
if (fileInfo[i].Contains(configFlag) || isGetInfoFlag)
{
if (!isGetInfoFlag)
{
i++;
isGetInfoFlag = true;
}
if (i < fileInfo.Length && !string.IsNullOrEmpty(fileInfo[i]))
{
infoList.Add(GetFontInfoByStr(fileInfo[i]));
}
}
}
return infoList;
}
private FontInfo GetFontInfoByStr(string str)
{
string[] strTemp = str.Split(',');
FontInfo fontInfo = new FontInfo();
string strPathTemp = string.Empty;
for (int i = 0; i < strTemp.Length; i++)
{
if (IsOddDoubleQuota(strTemp[i]))
{
strPathTemp += strTemp[i] + ",";
if (!IsOddDoubleQuota(strPathTemp))
{
strPathTemp = strPathTemp.Substring(0, strPathTemp.Length - 1);
break;
}
}
else
{
strPathTemp = strTemp[i];
break;
}
}
fontInfo.ImgPath = strPathTemp.Replace("icon=\"", string.Empty).Replace("\"", string.Empty);
fontInfo.charInfo = (char)int.Parse(strTemp[strTemp.Length - 4]);
return fontInfo;
}
private bool IsOddDoubleQuota(string str)
{
return GetDoubleQuotaCount(str) % 2 == 1;
}
private int GetDoubleQuotaCount(string str)
{
string[] strArray = str.Split('"');
int doubleQuotaCount = strArray.Length - 1;
doubleQuotaCount = doubleQuotaCount < 0 ? 0 : doubleQuotaCount;
return doubleQuotaCount;
}
private string FormatPath(string path)
{
path = path.Replace("\\", "/");
return path;
}
private Rect GetUV(XmlNode node, Texture2D textureFile)
{
Rect uv = new Rect
{
x = ToFloat(node, "x") / textureFile.width,
y = ToFloat(node, "y") / textureFile.height,
width = ToFloat(node, "width") / textureFile.width,
height = ToFloat(node, "height") / textureFile.height
};
uv.y = 1f - uv.y - uv.height;
return uv;
}
private Rect GetVert(XmlNode node)
{
Rect uv = new Rect
{
x = ToFloat(node, "xoffset"),
y = ToFloat(node, "yoffset"),
width = ToFloat(node, "width"),
height = ToFloat(node, "height")
};
uv.y = -uv.y;
uv.height = -uv.height;
return uv;
}
private int ToInt(XmlNode node, string name)
{
return Convert.ToInt32(node.Attributes.GetNamedItem(name).InnerText);
}
private float ToFloat(XmlNode node, string name)
{
return (float)ToInt(node, name);
}
static bool IsNullOrEmpty<T>(List<T> lst)
{
if (lst == null)
{
return true;
}
if (lst.Count == 0)
{
return true;
}
return false;
}
static T ToFind<T>(List<T> obj, Predicate<T> predicate)
{
for (int i = 0; i < obj.Count; i++)
{
if (predicate(obj[i]))
{
return obj[i];
}
}
return default;
}
}