Unity 位图字体工具

Unity 位图字体工具

原创文章,转载请注明出处:Unity 位图字体工具

Unity Editor CS 源码在最后

工具地址:
www.xxasync.com - 中文
www.xxasync.com - 英文

重要提示:如果图片解析出来的字符不准确,可以尝试调整 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;
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wx771720

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值