用Unity自身API的BitMap生成工具

前言

        一开始我准备写这个工具的时候,我还没发现原来已经有人这么做了。直到我做好了这个工具,向朋友说明的时候,朋友和我说网上已经有这样的工具了。我后面也搜索了一下,有部分是用第三方工具做的,有些则是直接使用Unity自带的API。大家可以直接搜索应该就可以搜到,大部分都是有着高阅读量的文章比较容易搜索到。

        我看过他们的效果,看起来是比我写的好多了。所以我本来是不行写这篇文章,可是我想到自己忙活了这么久,我不写篇文章太对不起我花费的时间了。这篇就算当成一次记录我也要写一下。反正网络上的水文那么多,也不差我这一篇

第三方工具 + Unity生成BitMap的代码

        在说明如何使用Unity自身API进行生成时,我先介绍一下如何使用第三方工具 + Unity生成BitMap。

        我自己个人的方法是先用第三方工具bmfont生成位图的图集和并得到一个配置信息的.fnt文件。然后我将bmfont生成的图集和配置文件都导入进Unity。我再通过下面的代码(下面的代码是同事给我,具体的出处我也找不到)配置好信息后运行,便得到了具有BitMap的Unity字体文件和其对应的材质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using Unity.EditorCoroutines.Editor;
using UnityEditor;
using UnityEngine;

namespace ToolEidtor
{
public class BitMapEidtorExtraTool : EditorWindow
{
[MenuItem("Tool/BitMap/ExtraTool")]
private static void ShowAtlasEditorWindow()
{
var window = GetWindow<BitMapEidtorExtraTool>();
window.autoRepaintOnSceneChange = true;
window.Show();
}

private SerializedObject serObj;
[SerializeField]
private Font font;
[SerializeField]
private TextAsset textAsset;
[SerializeField]
private Texture texture;
private SerializedProperty fontPro;
private SerializedProperty textAssetPro;
private SerializedProperty texturePro;
private bool running;

private void OnEnable()
{
serObj = new SerializedObject(this);
fontPro = serObj.FindProperty("font");
textAssetPro = serObj.FindProperty("textAsset");
texturePro = serObj.FindProperty("texture");
running = false;
}

private void OnGUI()
{
EditorGUILayout.LabelField("字体");
EditorGUILayout.PropertyField(fontPro, true);
EditorGUILayout.LabelField("配置文本");
EditorGUILayout.PropertyField(textAssetPro, true);
EditorGUILayout.LabelField("对应的图片");
EditorGUILayout.PropertyField(texturePro, true);
serObj.ApplyModifiedProperties();
if (font == null || textAsset == null || texture == null)
{
EditorGUILayout.HelpBox("请设置好font、textAsset和texture", MessageType.Error);
}
else if (!running)
{
if (GUILayout.Button("开始生成"))
{
running = true;
EditorCoroutineUtility.StartCoroutineOwnerless(GenerateBitMap());
}
}
}

private IEnumerator GenerateBitMap()
{
XmlDocument xmlDocument = new XmlDocument();
xmlDocument.LoadXml(textAsset.text);

int totalWidth = Convert.ToInt32(xmlDocument["font"]["common"].Attributes["scaleW"].InnerText);
int totalHeight = Convert.ToInt32(xmlDocument["font"]["common"].Attributes["scaleH"].InnerText);

XmlElement xml = xmlDocument["font"]["chars"];
List<CharacterInfo> characterInfoList = new List<CharacterInfo>();
float all = xml.ChildNodes.Count;
Debug.Log(all);
for (int i = 0; i < xml.ChildNodes.Count; ++i)
{
if (EditorUtility.DisplayCancelableProgressBar("进度", "正在生成", i / all))
yield break;
XmlNode node = xml.ChildNodes[i];
if (node.Attributes == null)
{
continue;
}
int index = Convert.ToInt32(node.Attributes["id"].InnerText);
int x = Convert.ToInt32(node.Attributes["x"].InnerText);
int y = Convert.ToInt32(node.Attributes["y"].InnerText);
int width = Convert.ToInt32(node.Attributes["width"].InnerText);
int height = Convert.ToInt32(node.Attributes["height"].InnerText);
int xOffset = Convert.ToInt32(node.Attributes["xoffset"].InnerText);
int yOffset = Convert.ToInt32(node.Attributes["yoffset"].InnerText);
int xAdvance = Convert.ToInt32(node.Attributes["xadvance"].InnerText);
CharacterInfo info = new CharacterInfo();
Rect uv = new Rect();
uv.x = (float)x / totalWidth;
uv.y = (float)(totalHeight - y - height) / totalHeight;
uv.width = (float)width / totalWidth;
uv.height = (float)height / totalHeight;
info.index = index;
info.uvBottomLeft = new Vector2(uv.xMin, uv.yMin);
info.uvBottomRight = new Vector2(uv.xMax, uv.yMin);
info.uvTopLeft = new Vector2(uv.xMin, uv.yMax);
info.uvTopRight = new Vector2(uv.xMax, uv.yMax);
info.minX = xOffset;
info.maxX = xOffset + width;
info.minY = -yOffset - height;
info.maxY = -yOffset;
info.advance = xAdvance;
info.glyphWidth = width;
info.glyphHeight = height;
characterInfoList.Add(info);
yield return new WaitForEndOfFrame();
}
font.characterInfo = characterInfoList.ToArray();
Material material = new Material(Shader.Find("GUI/Text Shader"));
material.SetTexture("_MainTex", texture);
font.material = material;
string fontPath = AssetDatabase.GetAssetPath(font);
string path = Path.GetDirectoryName(fontPath);
string fileName = Path.GetFileNameWithoutExtension(fontPath);
string savePath = Path.Combine(path, fileName + ".mat");
AssetDatabase.CreateAsset(material, savePath);
EditorUtility.DisplayDialog("", "生成成功,新生成的材质在:" + savePath, "ok");
AssetDatabase.Refresh();
EditorUtility.ClearProgressBar();
running = false;
yield break;
}
}
}

bmfont + Unity生成BitMap的代码解析

        如果你没有用过第三方工具bmfont,网上有许多关于它的教程,你可以去使用一下看看。这样你会更能理解下面我说的事情。这里我们仔细分析一下上面这个部分代码做了什么事情。我们排除那些配置的信息和为了程序不卡死而做的操作,其实它最重要的代码就是GenerateBitMap函数中的代码。

        在GenerateBitMap函数中,我们先对bmfont提供的.fnt文件进行解析。虽然它是.fnt文件,但是它内部就是xml的格式。下面是我提供的.fnt文件的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0"?>
<font>
<info face="Arial" size="32" bold="0" italic="0" charset="" unicode="1" stretchH="100" smooth="1" aa="1" padding="0,0,0,0" spacing="1,1" outline="0"/>
<common lineHeight="32" base="26" scaleW="256" scaleH="256" pages="1" packed="0" alphaChnl="1" redChnl="0" greenChnl="0" blueChnl="0"/>
<pages>
<page id="0" file="RankNum_0.png" />
</pages>
<chars count="10">
<char id="48" x="0" y="0" width="33" height="43" xoffset="0" yoffset="0" xadvance="33" page="0" chnl="15" />
<char id="49" x="66" y="44" width="24" height="42" xoffset="0" yoffset="0" xadvance="24" page="0" chnl="15" />
<char id="50" x="0" y="44" width="32" height="42" xoffset="0" yoffset="0" xadvance="32" page="0" chnl="15" />
<char id="51" x="166" y="0" width="30" height="43" xoffset="0" yoffset="0" xadvance="30" page="0" chnl="15" />
<char id="52" x="197" y="0" width="34" height="42" xoffset="0" yoffset="0" xadvance="34" page="0" chnl="15" />
<char id="53" x="134" y="0" width="31" height="43" xoffset="0" yoffset="0" xadvance="31" page="0" chnl="15" />
<char id="54" x="68" y="0" width="32" height="43" xoffset="0" yoffset="0" xadvance="32" page="0" chnl="15" />
<char id="55" x="33" y="44" width="32" height="42" xoffset="0" yoffset="0" xadvance="32" page="0" chnl="15" />
<char id="56" x="34" y="0" width="33" height="43" xoffset="0" yoffset="0" xadvance="33" page="0" chnl="15" />
<char id="57" x="101" y="0" width="32" height="43" xoffset="0" yoffset="0" xadvance="32" page="0" chnl="15" />
</chars>
</font>

common中的scaleW和scaleH表示的就是bmfont生成出来的图集宽高。而下面的char表示具体是什么字符、对应的图片在图集的位置、偏移等信息。fnt文件中,我们也只会用到char和common,这些信息。主要我们还是看char内部的信息,common中的信息只是用来计算uv坐标的。

        我以上面的fnt中的第一个char为例子:id表示它所对应的字符,48就是'0'。x表示对应图片在图集中开始的x位置,y则同理。这里就是说它图片开始点位在(0,0)。width表示它图片占用的宽度,height则表示高度。xoffset和yoffset表示它开始点位的偏移度。xadvance表示从该字符原点到下一个字符原点的水平距离如果没什么特殊的操作,大家可以粗暴将其认为是x + width。page和chnl,这里我们并没有用到。因为某些原因,我不能给出图片。不过大家可以从它们的id中看出来,它们就是0-9。它们对应的图片排布如下:

1
2
0869534
271

所以在0旁边是8,在0下面的是2。所以8对应的x是34,刚好于0(x结束的位置:0 + 33 = 33)的图片相挨着。而2的y则是44,也是刚好于0(y结束的位置:0 + 43 = 43)的图片相挨着。

        当我们拥有这些信息后,我们就可以去配置CharacterInfo的内容。CharacterInfo是Unity自带的类,它就是有关如何从字体纹理渲染字符的规范。我们将其配置好后给Font,在加上对应的材质。我们就可以实现bitmap。而有关其更加具体的信息,大家还是去看一下Unity官方文档上的描述,其官方文档为:CharacterInfo。我觉得看完这些文档中的描述后,大部分的内容是可以看得懂了。我自己也有些没有看懂的地方,但是我觉得问题不大。

一点废话

        上面的代码已经可以完成了BitMap的生成了。但是我不得不去第三方的工具中生成,然后再到Unity中进行操作。最终我才得到我想要的东西。我是觉得有些繁琐,我认为这些东西明明可以在Unity中直接完成。所以我就开始写了这份工具。

        这其中还有一点原因,我个人认为bmfont使用起来有些麻烦。如果我较长时间不使用bmfont,我就会忘记如何操作。然后我就要浪费一定的时间去找一下教程并重新学一下。即使教程随便都能搜到,但是我重新去学也是很麻烦。当然我自己写的工具也可能对你来说有这样的问题。所以我也打算把这份思路一起分享出来,大家如果想自己写一份也可以按照这样的思路去写。然后你将UI设计成你喜欢的样子就好了。至于我,这工具毕竟是我写出来。我无论再健忘也会有一定的记忆吧。预防万一我还写了这篇文章来帮我回忆。

用Unity自身API生成BitMap

        如果我们要使用Unity自身的API去生成,那么我们就要完成bmfont的工作。即我们要生成图集和fnt文件内的内容。我们可以不用真的去生成fnt文件。因为我们生成后也是进行解析的,所以我们完全可以直接生成这些信息然后填充CharacterInfo。从上面我们对fnt文件内容的分析,我们可以知道fnt内部的信息几乎是由我们生成的图集决定的,除了像是xoffset和yoffset这样的内容。所以我们真正的工作只有生成图集,而将xoffset和yoffset进行单独进行额外配置操作。

PS:在bmfont中xoffset和yoffset的设置并不会影响到其他参数。这个最后只会影响CharacterInfo中的数据。其实我自己实际开发中xoffset和yoffset的数值一直为0。我也几乎没有对这两个参数进行额外的定义。

        我这里生成图集的思路比较简单就是针对图片的数量生成一张大图。我会尽量让他们是正方形的样式。但是我的算法有一个问题,如果图片的数量拆分的因数差距过大,那么图片的大小就会过长或是过宽。比如11这样的质数,那么图集的图片分布就会是1*11这样,以数字0-10为例子,它们就会按照下面的方式排列:

1
012345678910

之后代码会找到它们对应的图片大小,并且用它们中最大的图片大小来生成大图。比如最大的图片大小为100100,那么上面的例子中生成的大图大小就是1001100。最后让这些图片居中填充进去。然后我再根据图集信息来生成我们要填充到CharacterInfo的内容。

2024.7.20补充:Unity自带的Font类中不存在修改LineSpace的变量。然后我在网上查找到了这篇文章:ugui位图字体使用 - fnt生成fontsettings工具,文章中使用了SerializedObject来修改LineSpace值。下面实现中以加入,但是我是粗暴的将LineSpace值设定为图片的最大高度。大家可以根据自己的需要在生成文字后再次修正LineSpace值,以此来达到自己的需求。

具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Unity.EditorCoroutines.Editor;
using UnityEditor;
using UnityEngine;

namespace ToolEidtor
{
public class BitMapEditorSelfTool : EditorWindow
{
[Serializable]
private class ImgInfo
{
// 映射字符
public char mapChar;
public Texture2D texture;
public Vector2Int offset;

[NonSerialized]
public Vector2Int size;
[NonSerialized]
public Vector2Int startPos;
}

[MenuItem("Tool/BitMap/SelfTool")]
private static void ShowAtlasEditorWindow()
{
var window = GetWindow<BitMapEditorSelfTool>();
window.autoRepaintOnSceneChange = true;
window.Show();
}

private SerializedObject serObj;
private SerializedProperty imgPro;
private SerializedProperty imgInfoPro;
[SerializeField]
private List<ImgInfo> imgInfos;
[SerializeField]
private List<Texture2D> imgs;

private bool startExcute = false;

private Vector2 scrollPos;
private float flagH = 0;

// 完整路径
private string saveUrl;
// 相对路径
private string relateUrl;
private string relateUrlHead;


private void OnEnable()
{
imgInfos = new List<ImgInfo>();
imgs = new List<Texture2D>();
serObj = new SerializedObject(this);
imgInfoPro = serObj.FindProperty("imgInfos");
imgPro = serObj.FindProperty("imgs");
saveUrl = Application.dataPath;
relateUrlHead = saveUrl.Substring(0, saveUrl.Length - 6);
relateUrl = Path.GetRelativePath(relateUrlHead, saveUrl);
}

private void OnGUI()
{
// GUI开始前进行更新,这样在代码中改变的值才会作用到SerializedProperty
serObj.Update();
if (startExcute)
{
SetPropertyField(imgInfoPro, new GUIContent("图片信息"), flagH >= 300, 300, ref scrollPos, ref flagH);
EditorGUILayout.LabelField("选择你要保存的位置,显示的是你当前项目的相对位置");
EditorGUILayout.BeginHorizontal();
EditorGUILayout.TextField(relateUrl);
if(GUILayout.Button("...",GUILayout.MaxWidth(50)))
{
string path = EditorUtility.OpenFolderPanel("选择路径", relateUrlHead, "Assets");
if(!string.IsNullOrEmpty(path))
{
if(path.Contains(relateUrlHead))
{
relateUrl = Path.GetRelativePath(relateUrlHead, path);
saveUrl = path;
}
else
{
EditorUtility.DisplayDialog("选择路径有错", "请选择你项目下的文件", "OK");
}
}
}
EditorGUILayout.EndHorizontal();
// 执行的时候编辑器会报Assertion failed on expression: 'i->previewArtifactID == found->second.previewArtifactID'这个错误,不知道为什么
if (GUILayout.Button("一键生成"))
{
// 预先对值进行判断
Dictionary<char, List<int>> repeatRecord = new Dictionary<char, List<int>>();
bool existRepeat = false;
for(int i = 0;i < imgInfos.Count; ++i)
{
var imgInfo = imgInfos[i];
if (repeatRecord.ContainsKey(imgInfo.mapChar))
{
existRepeat = true;
repeatRecord[imgInfo.mapChar].Add(i);
}
else repeatRecord.Add(imgInfo.mapChar,new List<int>() { i});
}
// 如果存在重复发出提示
if(existRepeat)
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("字符必须唯一!\n");
foreach (var i in repeatRecord.Keys)
{
var lst = repeatRecord[i];
if(lst.Count > 1)
{

stringBuilder.Append(string.Format("字符串{0}在下标", i));
stringBuilder.Append(lst[0].ToString());
for (int j = 1;j < lst.Count; ++j)
{
stringBuilder.Append(',');
stringBuilder.Append(lst[j].ToString());
}
stringBuilder.Append("已经重复了\n");
}
}
EditorUtility.DisplayDialog("存在重复字符", stringBuilder.ToString(), "OK");
}
else
{
EditorCoroutineUtility.StartCoroutineOwnerless(GenerateBitMap());
}
}
}
else
{
var proFieldMaxH = 216 + EditorGUIUtility.singleLineHeight * 2;
SetPropertyField(imgPro, new GUIContent("所需图片"), flagH >= proFieldMaxH, proFieldMaxH, ref scrollPos, ref flagH);

if (GUILayout.Button("开始"))
{
// 设置imginfos
foreach (var img in imgs)
{
imgInfos.Add(new ImgInfo()
{
mapChar = img.name[0],
texture = img
});
}
startExcute = true;
scrollPos = Vector2.zero;
}

}
//在结束的时候进行,这样SerializedProperty的值才会作用到我们想要的变量上
serObj.ApplyModifiedProperties();
}

private void SetPropertyField(SerializedProperty pro,GUIContent label,bool needScroll,float scrollHeight, ref Vector2 curScrollPos,ref float fieldHeight)
{
if(needScroll)
{
curScrollPos = EditorGUILayout.BeginScrollView(curScrollPos, GUILayout.MaxHeight(scrollHeight));
EditorGUILayout.PropertyField(pro, label, true);
GetLastRectHeight(ref fieldHeight);
EditorGUILayout.EndScrollView();
}
else
{
curScrollPos = Vector2.zero;
EditorGUILayout.PropertyField(pro, label, true);
GetLastRectHeight(ref fieldHeight);
}
}

// 获取最后一个rect的高度值
// 有时候会变得非常小。
// 我猜测是EditorGUILayout和GUILayout同步时间不一致导致的。
// 所以我这边给了一个最小的检测值
private void GetLastRectHeight(ref float curVal,float minVal = 18)
{
var rect = GUILayoutUtility.GetLastRect();
if(rect != null)
{
if (rect.size.y >= minVal)
curVal = rect.size.y;
}
}
//bool test = true;
private IEnumerator GenerateBitMap()
{
int progressCount = imgInfos.Count * 3;
// 解析texture
Vector2Int maxSingleSize = new Vector2Int(0,0);
for(int i = 0;i < imgInfos.Count; ++i)
{
//获得包裹图片有效内容的最小矩形
var curInfo = imgInfos[i];
var rectInfo = GetImgMinRectangle(curInfo.texture);
curInfo.size = new Vector2Int((int)rectInfo.x, (int)rectInfo.y);
curInfo.startPos = new Vector2Int((int)rectInfo.z, (int)rectInfo.w);
maxSingleSize = Vector2Int.Max(maxSingleSize, curInfo.size);
}
var ratio = GetAlatWH(imgInfos.Count);
// 生成BitMap,并将信息设定完毕
int totalWidth = ratio.x * maxSingleSize.x;
int totalHeight = ratio.y * maxSingleSize.y;
// 这里是为了符合Unity对图像的压缩。即其大小必须被4整除
// 这里仍然有问题,如果你的图片的太大。那么在Unity压缩后的大小就不一定会被4整除了。不过我找不到如何解决的方法
int extraW = totalWidth % 4;
int extraH = totalHeight % 4;
if (extraW != 0)
{
totalWidth = (totalWidth / 4 + 1) * 4;
extraW = 4 - extraW;
}
if (extraH != 0)
{
totalHeight = (totalHeight / 4 + 1) * 4;
extraH = 4 - extraH;
}
Texture2D altas = new Texture2D(totalWidth, totalHeight);
List<CharacterInfo> fntInfo = new List<CharacterInfo>();
for(int i = 0;i < imgInfos.Count; ++i)
{
var curInfo = imgInfos[i];
var curSp = curInfo.texture;
int offsetX = (maxSingleSize.x - curInfo.size.x) / 2;
int offsetY = (maxSingleSize.y - curInfo.size.y) / 2;
int startX = i % ratio.x;
int startY = ratio.y - i / ratio.x - 1;
int minXRange = startX * maxSingleSize.x + offsetX;
int maxXRange = minXRange + curInfo.size.x;
int minYRange = startY * maxSingleSize.y + offsetY;
int maxYRange = minYRange + curInfo.size.y;
for (int j = startX * maxSingleSize.x; j < startX * maxSingleSize.x + maxSingleSize.x; ++ j)
{
for(int k = startY * maxSingleSize.y; k < startY * maxSingleSize.y + maxSingleSize.y; ++k)
{
if (j >= minXRange && j < maxXRange && k >= minYRange && k < maxYRange)
{
var col = curSp.GetPixel(j - minXRange + curInfo.startPos.x, k - minYRange + curInfo.startPos.y);
altas.SetPixel(j, k, col);
}
else altas.SetPixel(j, k, new Color(1,1,1,0));
}
}
CharacterInfo info = new CharacterInfo();
Rect uv = new Rect();
uv.x = 1.0f * minXRange / totalWidth;
uv.y = 1.0f * minYRange / totalHeight;
uv.width = curInfo.size.x * 1.0f / totalWidth;
uv.height = curInfo.size.y * 1.0f / totalHeight;
info.index = curInfo.mapChar;
info.uvBottomLeft = new Vector2(uv.xMin, uv.yMin);
info.uvBottomRight = new Vector2(uv.xMax, uv.yMin);
info.uvTopLeft = new Vector2(uv.xMin, uv.yMax);
info.uvTopRight = new Vector2(uv.xMax, uv.yMax);
info.advance = curInfo.size.x;
info.glyphWidth = curInfo.size.x;
info.glyphHeight = curInfo.size.y;
info.minX = curInfo.offset.x;
info.maxX = curInfo.offset.x + curInfo.size.x;
info.minY = -curInfo.offset.y - curInfo.size.y;
info.maxY = -curInfo.offset.y;
//info.size = 14;
fntInfo.Add(info);
}
// 将额外的部分变为透明
// 填充这里可以进行优化,但我偷懒了。但我在使用的时候发不填充也可以好好使用,且内存变化不大
for(int i = 0;i < extraW; ++i)
{
for(int j = 0; j < totalHeight; ++j)
altas.SetPixel(totalWidth - 1 - i, j, new Color(1, 1, 1, 0));
}
for (int i = 0; i < extraH; ++i)
{
for (int j = 0; j < totalWidth; ++j)
altas.SetPixel(j, totalHeight - 1 - i,new Color(1, 1, 1, 0));
}

altas.name = "BitMap";
string altasUrl = Path.Combine(saveUrl, "BitMap.png");
byte[] bytes = altas.EncodeToPNG();//转PNG
if (!File.Exists(altasUrl))
{
File.Create(altasUrl).Dispose();
}
File.WriteAllBytesAsync(altasUrl, bytes); // 存储

string fontFileUrl = Path.Combine(saveUrl, "BitMapFont.fontsettings");
if (!File.Exists(fontFileUrl))
{
// AssetDatabase.CreateAsset 只能用相对路径否则会报错
AssetDatabase.CreateAsset(new Font(), Path.Combine(relateUrl, "BitMapFont.fontsettings"));
AssetDatabase.Refresh();
yield return new EditorWaitForSeconds(0.2f);
}

var font = AssetDatabase.LoadAssetAtPath<Font>(Path.Combine(relateUrl, "BitMapFont.fontsettings"));
// 将其设定为需要修改
EditorUtility.SetDirty(font);
font.characterInfo = fntInfo.ToArray();

// 修改行高
SerializedObject serializedObject = new SerializedObject(font);
serializedObject.FindProperty("m_LineSpacing").floatValue = maxSingleSize.y;

serializedObject.ApplyModifiedProperties();
serializedObject.UpdateIfRequiredOrScript();
serializedObject.SetIsDifferentCacheDirty();

AssetDatabase.Refresh();

var matTex = AssetDatabase.LoadAssetAtPath<Texture2D>(Path.Combine(relateUrl, "BitMap.png"));

if(File.Exists(Path.Combine(saveUrl, "BitMapMat.mat")))
{
Material material = AssetDatabase.LoadAssetAtPath<Material>(Path.Combine(relateUrl, "BitMapMat.mat"));
material.shader = Shader.Find("GUI/Text Shader");
material.SetTexture("_MainTex", matTex);
font.material = material;
}
else
{
Material material = new Material(Shader.Find("GUI/Text Shader"));
material.SetTexture("_MainTex", matTex);
font.material = material;
AssetDatabase.CreateAsset(material, Path.Combine(relateUrl, "BitMapMat.mat"));
}

AssetDatabase.SaveAssetIfDirty(font);

AssetDatabase.Refresh();

EditorUtility.DisplayDialog("生成成功", "已经成功了,如果你使用有问题记得找一下这段代码的编写者", "OK");
yield break;
}

private Vector4 GetImgMinRectangle(Texture2D tex)
{
Vector4 vector4 = new Vector4(tex.width, 0, 0, tex.height);
for (int j = 0; j < tex.width; ++j)
{
if (j > vector4.x)
break;
for (int k = 0; k < tex.height; ++k)
{
var color = tex.GetPixel(j, k);
if (color.a > 0)
{
vector4.x = Mathf.Min(vector4.x, j);
break;
}
}
}
for (int j = tex.height - 1; j > -1; --j)
{
if (j < vector4.y)
break;
for (int k = 0; k < tex.width; ++k)
{
var color = tex.GetPixel(k, j);
if (color.a > 0)
{
vector4.y = Mathf.Max(vector4.y, j);
break;
}
}
}
for (int j = tex.width - 1; j > -1; --j)
{
if (j < vector4.z)
break;
for (int k = 0; k < tex.height; ++k)
{
var color = tex.GetPixel(j, k);
if (color.a != 0)
{
vector4.z = Mathf.Max(vector4.z, j);
break;
}
}
}
for (int j = 0; j < tex.height; ++j)
{
if (j > vector4.w)
break;
for (int k = 0; k < tex.width; ++k)
{
var color = tex.GetPixel(k, j);
if (color.a != 0)
{
vector4.w = Mathf.Min(vector4.w, j);
break;
}
}
}
Vector4 rectInfo = new Vector4(vector4.z - vector4.x, vector4.y - vector4.w, vector4.x, vector4.w);
return rectInfo;
}

private Vector2Int GetAlatWH(int frameCount)
{
int sq = Mathf.CeilToInt(Mathf.Sqrt(frameCount));
for (int i = sq; i > 0; --i)
{
if ((frameCount % i) == 0)
{
// 个人习惯
// 其实不一定要这样,你直接 return new Vector2Int(i, frameCount / i); 也可以
int val = Mathf.Max(i, frameCount / i);
return new Vector2Int(val, frameCount / val);
//return new Vector2Int(frameCount, 1);
}
}
return Vector2Int.one;
}
}
}

2024.2.23修改:修复了font修改后没有写入文件的bug,优化了部分代码的实现 2024.7.20修改:修复了font修改后没有写入LineSpace导致不能换行的问题

为了操作界面简单并且符合我的操作习惯,我写了很多界面方面的东西。但是其中的思想就是仿造bmfont的fnt信息然后对CharacterInfo进行填充,并生成对应的图集、材质和字体文件。

与第三方工具的差距

        显然我写的工具还是有挺多的问题。比如之前我说过的图集生成的问题,又或者是一些界面上的优化。我们这里并没有像bmfont一样可以生成我们想要的图片大小,并进行填充。这个其实我也有思路,Unity官方其实提供了一个根据大图大小打包图集的API:Texture2D.GenerateAtlas。它可以返回你给的条件是否能装下全部的图片。如果可以的话,它会将这些图片如何摆放的结果赋值给你的(List

总结

        这个工具本身可以优化的地方有很多,比如一些代码方面的优化、界面上的改动和图集的生成方式等。一开始我去做的时候,我并没有想那么多。后面我想到的时候,朋友就告诉我网上的工具了。如果没有其他的因素,我大概也不会去优化这份代码了吧。反正我已经有别人的工具了,我就不想再自己造轮子了。这篇文章就当做是一份思路的记录吧。