在游戏开发中,UI界面的文字展示往往需要更加个性化的视觉效果。普通的系统字体虽然使用方便,但样式单一,难以满足游戏美术风格的需求。这时候我们就需要用到艺术字——将文字设计成图片形式,再通过技术手段转换为可动态使用的字体资源。
我遇到过很多项目,美术同学精心设计了各种酷炫的文字效果,但最终实现时却遇到各种问题:要么是每个文字都要单独做成图片,资源管理混乱;要么是动态文本无法使用艺术效果,只能使用静态图片。这些问题都会严重影响开发效率和游戏表现力。
使用Unity自带的Custom Font功能配合Sprite图集,可以很好地解决这个问题。但手动配置字符映射关系非常繁琐,特别是当字符数量较多时,很容易出错。这就是为什么我们需要开发一个自动化工具,能够一键将Sprite图集转换为可用的字体资源。
首先需要美术同学提供一张包含所有需要使用的字符的图片。这张图片应该满足以下要求:
在实际项目中,我建议让美术使用Photoshop或类似工具,按照网格布局设计字符。比如26个字母+10个数字可以排成6x6的网格,这样后续处理会方便很多。
将图片导入Unity后,我们需要使用Sprite Editor进行分割:
这里有个小技巧:如果字符间距不一致,可以先用"Automatic"模式尝试自动识别,再手动调整不准确的分割线。我在一个项目中遇到过字符间距不规则的情况,自动分割效果不理想,最后通过半自动方式解决了问题。
首先创建一个自定义编辑器窗口,作为工具的主界面:
csharp复制using UnityEditor;
using UnityEngine;
public class ArtFontGenerator : EditorWindow
{
[MenuItem("Tools/Art Font Generator")]
static void Init()
{
var window = GetWindow<ArtFontGenerator>();
window.titleContent = new GUIContent("Art Font Generator");
window.Show();
}
void OnGUI()
{
// 主界面UI代码
}
}
这个窗口将包含以下主要功能区域:
关键的一步是读取Sprite Editor分割好的子图信息。Unity 2022提供了新的API来获取这些数据:
csharp复制var texFact = new SpriteDataProviderFactories();
texFact.Init();
var texDataProvider = texFact.GetSpriteEditorDataProviderFromObject(texture2d);
texDataProvider.InitSpriteEditorDataProvider();
var spriteRects = texDataProvider.GetSpriteRects();
这段代码可以获取到图集中每个字符的矩形区域信息。需要注意的是,不同Unity版本API可能有所变化,我在Unity 2019上就遇到过兼容性问题,最终通过条件编译解决了。
将Sprite信息转换为字体需要的CharacterInfo:
csharp复制CharacterInfo[] charInfoArr = new CharacterInfo[chars.Length];
for (int i = 0; i < chars.Length; i++)
{
var spRect = spriteRects[i].rect;
var uvMin = spRect.min / texSize;
var uvMax = spRect.max / texSize;
charInfoArr[i] = new CharacterInfo
{
index = chars[i],
uvBottomLeft = uvMin,
uvBottomRight = new Vector2(uvMax.x, uvMin.y),
uvTopLeft = new Vector2(uvMin.x, uvMax.y),
uvTopRight = uvMax,
advance = (int)(spRect.width * scale),
glyphWidth = (int)(spRect.width * scale),
glyphHeight = (int)(spRect.height * scale)
};
}
这里有几个关键点需要注意:
有了CharacterInfo数组后,就可以创建真正的字体资源了:
csharp复制Font newFont = new Font("ArtFont");
newFont.characterInfo = charInfoArr;
string path = "Assets/ArtFont.fontsettings";
AssetDatabase.CreateAsset(newFont, path);
为了让字体正确显示,还需要创建并配置材质:
csharp复制Material fontMat = new Material(Shader.Find("UI/Default Font"));
fontMat.mainTexture = charsTexture;
string matPath = "Assets/ArtFont.mat";
AssetDatabase.CreateAsset(fontMat, matPath);
newFont.material = fontMat;
这里使用的"UI/Default Font"着色器是Unity内置的,适合大多数情况。如果需要有特殊效果,比如描边或发光,可以自定义着色器。
艺术字的一个限制是无法动态调整大小,因为它们是图片。解决方案是为每个需要的字号生成单独的字体文件,但共享材质和纹理:
csharp复制for (int size = 12; size <= 48; size += 4)
{
Font sizedFont = Instantiate(newFont);
sizedFont.characterInfo = ResizeCharacterInfo(charInfoArr, baseSize, size);
string sizedPath = $"Assets/ArtFont_{size}.fontsettings";
AssetDatabase.CreateAsset(sizedFont, sizedPath);
}
这样可以保持Draw Call不增加,同时支持多种字号。我在一个手游项目中使用了这个方案,成功支持了从12px到48px共10种字号,性能表现很好。
TextMeshPro提供了更强大的文本渲染功能,但它的字体资源结构也更复杂。主要区别在于:
将之前生成的CharacterInfo转换为TMP需要的Glyph:
csharp复制Glyph glyph = new Glyph
{
index = (uint)charInfo.index,
metrics = new GlyphMetrics(
charInfo.glyphWidth,
charInfo.glyphHeight,
0,
charInfo.glyphHeight,
charInfo.advance
),
glyphRect = new GlyphRect(
(int)(charInfo.uvBottomLeft.x * atlasWidth),
(int)(charInfo.uvBottomLeft.y * atlasHeight),
charInfo.glyphWidth,
charInfo.glyphHeight
)
};
使用TMP的API创建字体资源:
csharp复制TMP_FontAsset fontAsset = TMP_FontAsset.CreateFontAsset(
baseFont,
fontSize,
0,
GlyphRenderMode.SMOOTH,
atlasWidth,
atlasHeight
);
fontAsset.atlas = charsTexture;
fontAsset.glyphTable.Add(glyph);
fontAsset.characterTable.Add(new TMP_Character(unicode, glyph));
AssetDatabase.CreateAsset(fontAsset, "Assets/ArtFont_TMP.asset");
在实际使用中,我发现TMP字体对字符间距的控制更精细,特别适合需要复杂排版的场景。
在工具中添加预览功能非常有用,可以实时查看字符与图片的对应关系:
csharp复制EditorGUILayout.BeginHorizontal();
{
// 显示图集
EditorGUILayout.LabelField(new GUIContent(charsTexture),
GUILayout.Width(256), GUILayout.Height(256));
// 显示选中的字符及其对应的图块
if (selectedChar >= 0)
{
var rect = spriteRects[selectedChar].rect;
var texRect = new Rect(rect.x / texWidth, 1 - rect.yMax / texHeight,
rect.width / texWidth, rect.height / texHeight);
EditorGUILayout.LabelField(new GUIContent(chars[selectedChar].ToString()),
GUILayout.Width(50));
EditorGUILayout.LabelField(new GUIContent(charsTexture, texRect),
GUILayout.Width(50), GUILayout.Height(50));
}
}
EditorGUILayout.EndHorizontal();
这个功能在我们处理非连续字符集(比如只使用部分符号)时特别有用,可以避免字符与图片错位的问题。
为了优化性能,应该让不同字号的字体共享材质:
csharp复制Material sharedMat = AssetDatabase.LoadAssetAtPath<Material>("Assets/ArtFont.mat");
foreach (var font in generatedFonts)
{
font.material = sharedMat;
EditorUtility.SetDirty(font);
}
这样可以确保所有字体使用同一个材质,减少Draw Call。我在一个UI复杂的项目中应用这个技巧,将Draw Call从30+降低到了5个左右。
工具可以自动从文本文件或输入框中提取使用的字符:
csharp复制string charSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
string inputText = "HELLO WORLD";
HashSet<char> uniqueChars = new HashSet<char>(charSet.ToCharArray());
uniqueChars.UnionWith(inputText.ToUpper().ToCharArray());
char[] finalChars = uniqueChars.ToArray();
Array.Sort(finalChars);
这个功能特别适合本地化文本,可以自动收集所有需要的字符,避免生成不必要的字体数据。
这是最常见的问题,通常有以下几种原因:
我建议在工具中添加调试视图,可以直观地看到每个字符的UV映射情况。
当艺术字放大时可能出现模糊,解决方法:
csharp复制TextureImporter importer = AssetImporter.GetAtPath(assetPath) as TextureImporter;
importer.filterMode = FilterMode.Point;
importer.mipmapEnabled = false;
AssetDatabase.ImportAsset(assetPath);
对于大量使用艺术字的项目,可以考虑以下优化:
在一个文字量大的RPG项目中,我们实现了按场景加载字体资源的机制,内存使用降低了40%。
艺术字技术不仅可以用于游戏UI,还可以应用于:
我曾经用这个技术实现了一个卡牌游戏,其中所有卡牌名称和特效描述都使用艺术字,配合Shader实现了金属质感、发光等效果,大大提升了视觉表现力。
开发过程中最大的收获是认识到工具化的重要性。最初我们手动配置每个字符,效率极低还容易出错。后来开发了这个自动化工具后,艺术字制作时间从几小时缩短到几分钟,而且完全避免了人为错误。这再次验证了一个道理:好的工具不仅能提高效率,还能提升产品质量。