Unity 位图字体工具
原创文章,转载请注明出处:Unity 位图字体工具
Unity Editor CS 源码在最后
重要提示:如果图片解析出来的字符不准确,可以尝试调整 url 地址中的参数 "threshold=20",默认为 20。如果还是解析不准确,可以将该图片邮件 wx771720@outlook.com 发给我,我这边测试修复后会立即更新工具
## 最终效果 ![在这里插入图片描述](https://img-blog.csdnimg.cn/50cae01dbcb14d6391034cac39e97772.png)工具截图
- 将位图字图片资源拖曳到工具内
示例位图字图片资源.png
- 设置【字符】集后,点击【导出】
- 导出界面点击【保存】后,会生成一份 .xxfont 文件,将该文件导入到 Unity 资源目录中即可。
Unity 脚本
将以下脚本放到 Unity 中的 Editor 目录下
// 该文件为完整工具脚本,放到 Unity Editor 目录下即可
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using UnityEditor;
using UnityEngine;
namespace xx.font
{
/// <summary>
/// Unity 位图字体导入
/// <para>author wx771720@outlook.com 2022-12-08 11:32:23</para>
/// </summary>
public class XXFont : AssetPostprocessor
{
public static void OnPostprocessAllAssets(string[] imported, string[] deleted, string[] moved, string[] movedFromAssetPaths)
{
foreach (string path in imported)// 新增或者变更文件
{
if (EndsWith(path, ".xxfont")) //位图字体
ParseXXFont(path);
else if (EndsWith(path, ".png") && IsXXFontTexture(path)) //位图字体纹理
CreateFontAsset(path);
}
}
//-----------------------------------------------------------------------------
//工具方法
//-----------------------------------------------------------------------------
private static CultureInfo ENUSCultureInfo = new CultureInfo("en-US", false);
/// <summary>
/// 指定字符串是否已指定字符串结束(不区分大小写)
/// </summary>
private static bool EndsWith(string target, params string[] tests)
{
foreach (string test in tests)
{
if (target.EndsWith(test, true, ENUSCultureInfo)) return true;
}
return false;
}
private const string assetAutoRenameReg = @"\s+[0-9]+$";
/// <summary>
/// 尝试解析自动重命名的资源名
/// </summary>
public static bool TryParseAutoRename(string name, out string originName)
{
originName = name;
Match matched = Regex.Match(name, assetAutoRenameReg);
if (!matched.Success) return false;
originName = name.Substring(0, name.Length - matched.Value.Length);
return true;
}
//-----------------------------------------------------------------------------
//字体解析
//-----------------------------------------------------------------------------
//format : xxfont(string) + [type(1 byte) + size(4 byte) + content]
public const string XX_FONT_FLAG = "xxfont";
private const int XX_FONT_TYPE_CONFIG = 1;
private const int XX_FONT_TYPE_IMAGE = 2;
private static Dictionary<string, object> fontCfg;
public static bool IsXXFontTexture(string url)
{
string path = Path.GetDirectoryName(url);
string name = Path.GetFileNameWithoutExtension(url);
return File.Exists(Path.Combine(path, $"{name}.{XX_FONT_FLAG}"));
}
public static void ParseXXFont(string url)
{
string path = Path.GetDirectoryName(url);
string name = Path.GetFileNameWithoutExtension(url);
if (TryParseAutoRename(name, out string originName))
{
string originUrl = Path.Combine(path, $"{originName}.{XX_FONT_FLAG}");
if (File.Exists(Path.Combine(path, $"{originName}.{XX_FONT_FLAG}")))
{
AssetDatabase.DeleteAsset(Path.Combine(path, $"{name}.png"));
AssetDatabase.DeleteAsset(originUrl);
string result = AssetDatabase.RenameAsset(url, $"{originName}.{XX_FONT_FLAG}");
return;
}
}
if (!File.Exists(url))
{
Debug.LogWarning($"parse xx font not found error : {url}");
return;
}
using (MemoryStream stream = new MemoryStream(File.ReadAllBytes(url)))
{
using (BinaryReader reader = new BinaryReader(stream))
{
// 校验文件标记
for (int index = 0; index < XX_FONT_FLAG.Length; ++index)
{
if (XX_FONT_FLAG[index] != reader.ReadChar())
{
Debug.LogWarning($"parse xx font format error : {url}");
return;
}
}
//
byte type;
int length;
byte[] configBytes = null, imageBytes = null;
while (stream.Position < stream.Length)
{
type = reader.ReadByte();
length = reader.ReadInt32();
if (stream.Position + length > stream.Length)
{
Debug.LogWarning($"parse xx font format length error : {type} - {url}");
return;
}
switch (type)
{
case XX_FONT_TYPE_CONFIG:
configBytes = reader.ReadBytes(length);
break;
case XX_FONT_TYPE_IMAGE:
imageBytes = reader.ReadBytes(length);
break;
}
}
if (null == configBytes)
{
Debug.LogWarning($"parse xx font config miss error : {url}");
return;
}
if (null == imageBytes)
{
Debug.LogWarning($"parse xx font image miss error : {url}");
return;
}
// 配置
fontCfg = StringToJSON(Encoding.ASCII.GetString(configBytes)) as Dictionary<string, object>;
if (null == fontCfg) { Debug.LogWarning($"parse xx font config format error : {url}"); return; }
// 存储图片资源
string imageUrl = Path.Combine(path, $"{name}.png");
if (File.Exists(imageUrl)) File.Delete(imageUrl);
File.WriteAllBytes(imageUrl, imageBytes);
AssetDatabase.ImportAsset(imageUrl);
}
}
}
public static void CreateFontAsset(string imageUrl)
{
if (null == fontCfg) return;
string path = Path.GetDirectoryName(imageUrl);
string name = Path.GetFileNameWithoutExtension(imageUrl);
Texture2D texture = AssetDatabase.LoadAssetAtPath<Texture2D>(imageUrl);
string xxfontUrl = Path.Combine(path, $"{name}.xxfont");
// 创建材质
string materialUrl = Path.Combine(path, $"{name}.mat");
Material material;
if (File.Exists(materialUrl))
{
material = AssetDatabase.LoadAssetAtPath<Material>(materialUrl);
material.mainTexture = texture;
}
else
{
material = new Material(Shader.Find("UI/Default"));
material.mainTexture = texture;
AssetDatabase.CreateAsset(material, materialUrl);
}
// 创建字体
string fontUrl = Path.Combine(path, $"{name}.fontsettings");
Font font; ;
if (File.Exists(fontUrl))
{
font = AssetDatabase.LoadAssetAtPath<Font>(fontUrl);
}
else
{
font = new Font(name);
AssetDatabase.CreateAsset(font, fontUrl);
}
font.material = material;
List<object> chars = fontCfg["chars"] as List<object>;
if (null == chars)
{
Debug.LogWarning($"parse xx font config.chars format error : {xxfontUrl}");
return;
}
List<CharacterInfo> charInfoList = new List<CharacterInfo>();
foreach (var charCfg in chars)
{
Dictionary<string, object> charCfgMap = charCfg as Dictionary<string, object>;
CharacterInfo charInfo = new CharacterInfo();
charInfo.index = (int)charCfgMap["id"];
Rect uv = new Rect();
uv.x = (float)((int)charCfgMap["x"]) / (int)fontCfg["width"];
uv.y = (float)((int)fontCfg["height"] - (int)charCfgMap["y"] - (int)charCfgMap["height"]) / (int)fontCfg["height"];
uv.width = (float)((int)charCfgMap["width"]) / (int)fontCfg["width"];
uv.height = (float)((int)charCfgMap["height"]) / (int)fontCfg["height"];
charInfo.uvBottomLeft = new Vector2(uv.xMin, uv.yMin);
charInfo.uvBottomRight = new Vector2(uv.xMax, uv.yMin);
charInfo.uvTopLeft = new Vector2(uv.xMin, uv.yMax);
charInfo.uvTopRight = new Vector2(uv.xMax, uv.yMax);
charInfo.minX = (int)charCfgMap["xOffset"];
charInfo.maxX = (int)charCfgMap["xOffset"] + (int)charCfgMap["width"];
charInfo.minY = -(int)charCfgMap["yOffset"] - (int)charCfgMap["height"];
charInfo.maxY = -(int)charCfgMap["yOffset"];
charInfo.advance = (int)((int)charCfgMap["realWidth"] * (double)fontCfg["tracking"]);
charInfo.glyphWidth = (int)charCfgMap["width"];
charInfo.glyphHeight = (int)charCfgMap["height"];
charInfo.size = (int)fontCfg["size"];
charInfoList.Add(charInfo);
}
font.characterInfo = charInfoList.ToArray();
SerializedObject so = new SerializedObject(font);
so.FindProperty("m_LineSpacing").floatValue = (int)fontCfg["lineHeight"];
// so.FindProperty("m_Tracking").floatValue = (int)fontCfg["tracking"];
so.ApplyModifiedProperties();
UnityEditor.EditorUtility.SetDirty(font);
AssetDatabase.SaveAssets();
}
//-----------------------------------------------------------------------------
//JSON
//-----------------------------------------------------------------------------
private const char jsonString = '"';
private const char jsonEscape = '\\';
private const char jsonComma = ',';
private const char jsonColon = ':';
private const char jsonListBegin = '[';
private const char jsonListEnd = ']';
private const char jsonDictionaryBegin = '{';
private const char jsonDictionaryEnd = '}';
public static object StringToJSON(string data)
{
int end;
object result;
try { if (TryJSONMeta(data, 0, out end, out result)) return result; } catch (Exception) { result = null; }
return result;
}
private static bool isJSONEmpty(char letter) { return ' ' == letter || '\r' == letter || '\n' == letter; }
private static bool TryJSONMeta(string data, int begin, out int end, out object result)
{
if (TryJSONString(data, begin, out end, out result)) return true;
if (TryJSONNumber(data, begin, out end, out result)) return true;
if (TryJSONBool(data, begin, out end, out result)) return true;
if (TryJSONNull(data, begin, out end)) { result = null; return true; }
if (TryJSONArray(data, begin, out end, out result)) return true;
if (TryJSONDictionary(data, begin, out end, out result)) return true;
return false;
}
private static Dictionary<char, char> jsonStrEscape = new Dictionary<char, char>() { { '\\', '\\' }, { '/', '/' }, { '"', '"' }, { 'f', '\f' }, { 't', '\t' }, { 'n', '\n' }, { 'r', '\r' } };
private static StringBuilder jsonStrBuilder = new StringBuilder();
private static bool TryJSONString(string data, int begin, out int end, out object result)
{
end = begin;
result = null;
char letter;
bool hasEscape = false;
int? stringBegin = null;
while (begin < data.Length)
{
letter = data[begin++];
if (stringBegin.HasValue)//已找到部分字符
{
if (letter == jsonEscape)//转义字符
{
if (begin >= data.Length) return false;
hasEscape = true;
letter = data[begin++];
if ('u' == letter || 'U' == letter) begin += 4;
}
else if (letter == jsonString)//已找到完整字符串
{
end = begin;
if (hasEscape)
{
jsonStrBuilder.Clear();
for (begin = stringBegin.Value; begin + 1 < end; begin++)
{
letter = data[begin];
if (jsonEscape == letter)
{
letter = data[++begin];
if ('u' == letter || 'U' == letter)
{
string str = new string(new char[] { data[++begin], data[++begin], data[++begin], data[++begin] });
jsonStrBuilder.Append(Convert.ToChar(Convert.ToInt32(str, 16)));
}
else if (jsonStrEscape.ContainsKey(letter)) jsonStrBuilder.Append(jsonStrEscape[letter]);
}
else jsonStrBuilder.Append(data[begin]);
}
result = jsonStrBuilder.ToString();
}
else result = data.Substring(stringBegin.Value, end - stringBegin.Value - 1);
return true;
}
}
else if (!isJSONEmpty(letter))//第一个可用字符
{
if (letter == jsonString) stringBegin = begin;
else return false;//不符合
}
}
return false;
}
private static char[] jsonInt32 = new char[] { '2', '1', '4', '7', '4', '8', '3', '6', '4' };
private static bool isJSONInt32(string data, int begin, int count, bool negative)
{
if (count < 10) return true;
if (count > 10) return false;
for (int index = 0; index < jsonInt32.Length; index++) { if (data[begin + index] > jsonInt32[index]) return false; }
begin += jsonInt32.Length;
if ((negative && data[begin] > '8') || (!negative && data[begin] > '7')) return false;
return true;
}
private static bool TryJSONNumber(string data, int begin, out int end, out object result)
{
end = begin;
result = null;
char letter;
int? dotIndex = null;
int? numberBegin = null;
while (begin < data.Length)
{
letter = data[begin++];
if (numberBegin.HasValue)//已找到部分字符
{
if ('.' == letter && !dotIndex.HasValue) dotIndex = begin - 1;//小数
else if (letter < '0' || letter > '9') { --begin; break; }//结束
}
else if (!isJSONEmpty(letter))//第一个可用字符
{
if ('-' == letter || ('0' <= letter && '9' >= letter)) numberBegin = begin - 1;
else return false;//不符合
}
}
if (numberBegin.HasValue)
{
bool isNegative = '-' == data[numberBegin.Value];
if (isNegative) numberBegin++;
end = begin;
if (dotIndex.HasValue)//小数
{
//小数点左右至少包含一位数字 && 整数部分至少两位时最左边不能为 0
if (dotIndex.Value > numberBegin && dotIndex.Value + 1 < end && ('0' != data[numberBegin.Value] || dotIndex.Value == numberBegin.Value + 1))
{
if (isNegative) --numberBegin;
result = Convert.ToDouble(data.Substring(numberBegin.Value, end - numberBegin.Value));
return true;
}
}
else if ('0' != data[numberBegin.Value] || 1 == end - numberBegin.Value)//整数(至少两位时最左边不能为 0)
{
if (isJSONInt32(data, numberBegin.Value, end - numberBegin.Value, isNegative))
{
if (isNegative) --numberBegin;
result = Convert.ToInt32(data.Substring(numberBegin.Value, end - numberBegin.Value));
return true;
}
else
{
if (isNegative) --numberBegin;
result = Convert.ToInt64(data.Substring(numberBegin.Value, end - numberBegin.Value));
return true;
}
}
}
return false;
}
private static char[] jsonTrue = new char[] { 't', 'r', 'u', 'e' };
private static char[] jsonFalse = new char[] { 'f', 'a', 'l', 's', 'e' };
private static bool TryJSONBool(string data, int begin, out int end, out object result)
{
end = begin;
result = false;
char letter;
int? trueIndex = null;
int? falseIndex = null;
while (begin < data.Length)
{
letter = data[begin++];
if (trueIndex.HasValue)//已找到 true 部分字符
{
if (letter != jsonTrue[trueIndex.Value]) return false;//不符合
trueIndex++;
if (trueIndex.Value == jsonTrue.Length) { end = begin; result = true; return true; }//已找到完整 true 字符串
}
else if (falseIndex.HasValue)//已找到 false 部分字符
{
if (letter != jsonFalse[falseIndex.Value]) return false;//不符合
falseIndex++;
if (falseIndex.Value == jsonFalse.Length) { end = begin; result = false; return true; }//已找到完整 false 字符串
}
else if (!isJSONEmpty(letter))//第一个可用字符
{
if (letter == jsonTrue[0]) trueIndex = 1;
else if (letter == jsonFalse[0]) falseIndex = 1;
else return false;//不符合
}
}
return false;
}
private static char[] jsonNull = new char[] { 'n', 'u', 'l', 'l' };
private static bool TryJSONNull(string data, int begin, out int end)
{
end = begin;
char letter;
int? nullIndex = null;
while (begin < data.Length)
{
letter = data[begin++];
if (nullIndex.HasValue)//已找到部分字符
{
if (letter != jsonNull[nullIndex.Value]) return false;//不符合
nullIndex++;
if (nullIndex.Value == jsonNull.Length) { end = begin; return true; }//已找到完整字符串
}
else if (!isJSONEmpty(letter))//第一个可用字符
{
if (letter == jsonNull[0]) nullIndex = 0;
else return false;//不符合
}
}
return false;
}
private static bool TryJSONArray(string data, int begin, out int end, out object result)
{
end = begin;
result = null;
char letter;
List<object> array = new List<object>();
bool arrayStart = false;
while (begin < data.Length)
{
letter = data[begin++];
if (arrayStart)//已找到部分字符
{
if (letter == jsonListEnd)//结束
{
end = begin;
result = array;
return true;
}
else if (!isJSONEmpty(letter) && jsonComma != letter)//可用字符
{
if (TryJSONMeta(data, begin - 1, out end, out result)) { begin = end; array.Add(result); }
else return false;
}
}
else if (!isJSONEmpty(letter))//第一个可用字符
{
if (letter == jsonListBegin) arrayStart = true;
else return false;//不符合
}
}
return false;
}
private static bool TryJSONDictionary(string data, int begin, out int end, out object result)
{
end = begin;
result = null;
char letter;
Dictionary<string, object> dictionary = new Dictionary<string, object>();
bool dictionaryStart = false;
bool keyTurn = true;
object key = null;
object value = null;
while (begin < data.Length)
{
letter = data[begin++];
if (dictionaryStart)//已找到部分字符
{
if (letter == jsonDictionaryEnd)//结束
{
end = begin;
result = dictionary;
return true;
}
else if (!isJSONEmpty(letter) && jsonComma != letter)//可用字符
{
if (jsonColon == letter)//键值对分隔字符
{
if (null == key) return false;
keyTurn = false;
}
else if (keyTurn)//键
{
if (TryJSONString(data, begin - 1, out end, out key)) { begin = end; }
else return false;
}
else//值
{
if (TryJSONMeta(data, begin - 1, out end, out value))
{
keyTurn = true;
begin = end;
dictionary[(string)key] = value;
}
else return false;
}
}
}
else if (!isJSONEmpty(letter))//第一个可用字符
{
if (letter == jsonDictionaryBegin) dictionaryStart = true;
else return false;//不符合
}
}
return false;
}
}
}