文章目录
一、前言
嗨,大家好,我是林新发。
我使用Unity
开发项目,然后发布成微信小游戏的时候遇到了一个问题:有部分中文字无法显示。
发现是因为个别界面预设中的Text
使用了默认的Arial
字体,为什么会出现中文字无法显示的问题呢?
我觉得有必要写篇文章讲讲。
二、Unity默认的字体:Arial
Unity
默认使用的字体是Arial
,比如你用UGUI
创建一个Text
,你就会看到它使用的Font
为Arial
,
Arial
是Windows
的系统字体之一,Unity
直接访问它,并以动态字体(Dynamic
)的形式渲染。
Arial
本身并不包含中文字库,为了证明Arial
字体不包含中文字库,我使用FontCreator
这个软件打开arial.ttf
字体,然后预览窗口中输入Hello,我叫林新发
,可以看到,中文字显示不了:
那Unity
是怎么把中文显示出来的呢?我贴一段Unity
官方手册的说明吧~
手册文档中提到的Include Font Data__
和Font Names__
字段,我们在Unity
中选择字体文件,在Inspector
面包中进行设置:
所以我们的中文并不是通过Arial
这个字体来渲染的,而是通过其他系统字体来渲染的。
三、查看动态字体的动态纹理
跟3D
模型的渲染类似,文字的渲染过程也是GPU
通过网格、纹理、材质等信息计算绘制出来的,
动态字体 是在运行时动态创建字的纹理,并且当出现字体库中不存在的字时,会从系统的默认字体库中查找对应的文字。
我们创建一个Text
,使用默认的Arial
动态字体,如下
我们可以看到英文和中文都能正常显示,
接着,我们打开Frame Debugger
,
可以看到渲染文字时它动态生成了一张Font Texture
,不过在Frame Debugger
中看不清,
没关系,我们可以通过代码把这张纹理图取出来显示到界面中。我们先创建一个RawImage
,用于显示字体纹理,
然后创建一个Main.cs
脚本,代码如下:
using UnityEngine;
using UnityEngine.UI;
public class Main : MonoBehaviour
{
public Text text;
public RawImage img;
void Start()
{
img.texture = text.font.material.mainTexture;
}
}
把Main.cs
脚本挂到Canvas
节点上,并赋值Text
和RawImage
对象,如下,
运行Unity
,现在可以看清字体的纹理啦,
现在我们来玩个好玩的,我动态调整Text
的字号,可以看到它动态生成了不同字号的纹理,如下
这就是为什么我们使用动态字体时,不同字号的字清晰度不同的原因,它会根据你字号所在的段位查找匹配的纹理进行渲染。
四、用动态字体还是静态字体
用动态字体还是静态字体,这个问题要具体情况具体分析。
1、用不用Arial动态字体
如果你的项目是纯英文的项目,你可以使用默认的Arial
动态字体,否则不要使用默认的Arial
动态字体,原因如下:
2、用Dynamic还是Custom set
自己导入一个TTF
字体,是用Dynamic
还是用Custom set
呢?
我们先来做个实验,以这个这个Dengb.ttf
字体为例,它有15M
这么大(实际项目不会用这么大的字体,会做一些处理,下文会讲解决办法),
假设我们使用Dynamic
,并且勾选的了Incl. Font Data
,如下,
现在我们使用Addressables
系统,把它单独放在一个Group
中,如下
现在们打包资源,看生成的.bundle
文件大小,有11.1M
那么大,
我们使用AssetStudio
逆向这个.bundle
文件,可以看到它包含了完整的字体文件数据和一张空纹理,
如果字体放在包内,你的包体就会变大,如果你是动态下载,那么就要下载十几兆的字体bundle
,这对于存储空间和资源下载都不是很友好,当然,如果你不在乎这点存储空间和下载时间,可以不用管~
五、字体文件资源瘦身
有没有缩小字体文件的办法呢?有两种解决办法。
1、办法一:字体文件本身做裁剪
对ttf
字体本身做裁剪瘦身,我之前写过一篇教程,可以参见我这篇文章:《字体裁剪,精简字体,字体瘦身:FontSubsetGUI,FontCreator,FontPruner》
字体设置上依然使用Dynamic
。
2、办法二:制作静态字体
制作静态字体,也就是使用Custom set
。下面我着重讲下这种办法。
注意,如果使用静态字体,当出现不存在的字形时,Unity
并不会像动态字体那样去帮我们查询后备字体和系统字体,所以会导致文字无法正常渲染。
所以这里就涉及到一个问题,我们使用Custom set
,要填写的Custom Chars
该填多少个字呢?
不管三七二十一,把所有的汉字都填进去,那是不科学的,咱们就拿1994
年出版的《中华字海》来说,它收字有85,568
个,全放进去,生成的纹理该有多大啊。
一般我们只会放常用的8000汉字
、英文字母、数字、标点符号等,设置Font Size
为60
,即使这样,生成的纹理尺寸已经达到极限的 4096 x 4096
,有16M
这么大,
可以酌情得把常用汉字减少一些,比如减到5000字
,另外,如果你非常确定所需要显示的字量,比如我非常明确只用到了Hello,林新发
这几个字,Custom Chars
中我就只需要填Hello,林新发
这几个字,
生成的纹理只有32KB
,
我们运行时动态修改Text
的字号,可以发现它始终都是引用静态字体的纹理,不会像动态字体那样动态生成纹理,字号的调整仅仅只是做纹理的缩放,当缩放过大时就会显得模糊,
另外,由于纹理是静态的,Text
的Font Style
只能是Normal
,不能设置斜体、粗体等,
现在,我们使用Addressables
系统重新打包资源,可以看到生成的.bundle
只有8.13KB
,
我们使用Asset Studio
逆向bundle
文件,可以看到里面存放这我们的32KB
纹理和470B
的字体信息,这样就大大减少了字体文件的资源大小了,
那么问题又来了,我们如何确定工程中到底用到了哪些字呢?我们可以写工具去扫描整个工程。
六、扫描工程中用到的字,自动设置Custom Chars
一般我们工程中使用到的文字,会散落在以下一些地方:配置表、代码写死的字符串、预设中摆放的Text
、预设中挂的MonoBehaviour
脚本的string
类型的成员变量等。
以下示例代码需引入的命名空间:
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using UnityEngine.UI;
using System.Text;
using System.Reflection;
1、配置表、代码文本扫描
配置表和代码的扫描,可以使用正则表达式匹配,如果懒的话,直接全部字符文本都读出来,以扫描json
配置表为例:
// 配置文本扫描
public string ScanJsonCfg()
{
StringBuilder sbr = new StringBuilder();
string[] fs = Directory.GetFiles(Application.dataPath + "/Config", "*.json", SearchOption.AllDirectories);
for (int i = 0, flen = fs.Length; i < flen; ++i)
{
var f = fs[i];
EditorUtility.DisplayProgressBar("扫描中", f, (float)i / flen);
var path = f.Replace(Application.dataPath, "Assets");
var cfg_text = AssetDatabase.LoadAssetAtPath<TextAsset>(path).text;
// TODO 可自行做正则表达式匹配
// ...
sbr.AppendLine(cfg_text);
}
EditorUtility.ClearProgressBar();
return sbr.ToString();
}
2、预设文本扫描
预设中文本的扫描,示例:
// 预设文本扫描
public string ScanPrefabText()
{
StringBuilder sbr = new StringBuilder();
string[] fs = Directory.GetFiles(Application.dataPath + "/Prefabs", "*.prefab", SearchOption.AllDirectories);
for (int i = 0, flen = fs.Length; i < flen; ++i)
{
var f = fs[i];
EditorUtility.DisplayProgressBar("扫描中", f, (float)i / flen);
var path = f.Replace(Application.dataPath, "Assets");
var go = AssetDatabase.LoadAssetAtPath<GameObject>(path);
var texts = go.GetComponentsInChildren<Text>(true);
for (int j = 0, len = texts.Length; j < len; ++j)
{
sbr.Append(texts[j].text);
}
// 通过反射获取所有脚本的public string的成员----------------------------------------
var triggers = go.GetComponentsInChildren<MonoBehaviour>(true);
for (int j = 0, len = triggers.Length; j < len; ++j)
{
var fields = triggers[j].GetType().GetFields(BindingFlags.Public | BindingFlags.Instance);
foreach (var field in fields)
{
if (field.FieldType == typeof(string))
{
var txt = (string)field.GetValue(triggers[j]);
sbr.Append(txt);
}
}
}
}
EditorUtility.ClearProgressBar();
return sbr.ToString();
}
3、通用标点符号、数字、字母
再补充一些标点符号、数字、字母啥的,
// 通用标点符号、数字、字母等
public string GetCommonString()
{
return @"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
, 、 。 . ? ! ~ $ % @ & # * ? ; ︰ … ‥ ﹐ ﹒ ˙ ? ‘ ’ “ ” 〝 〞 ‵ ′ 〃 ↑ ↓ ← → ↖ ↗ ↙ ↘
㊣ ◎ ○ ● ⊕ ⊙ ○ ● △ ▲ ☆ ★ ◇ ◆ □ ■ ▽ ▼ § ¥ 〒 ¢ £ ※ ♀ ♂
ΑΒΓΔΕΖΗΘΙΚ∧ΜΝΞΟ∏Ρ∑ΤΥΦΧΨΩ
α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω
АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ
а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я
ā á ǎ à、ō ó ǒ ò、ê ē é ě è、ī í ǐ ì、ū ú ǔ ù、ǖ ǘ ǚ ǜ ü
ˉˇ¨‘’々~‖∶”’‘|〃〔〕《》「」『』.〖〗【【】()〔〕{}[]~||¶µ©®ßΛΣΠ€♯♪♫
ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ①②③④⑤⑥⑦⑧⑨⑩
≈≡≠=≤≥<>≮≯∷±+-×÷/∫∮∝∞∧∨∑∏∪∩∈∵∴⊥‖∠⌒⊙≌∽√
§№☆★○●◎◇◆□■△▲※→←↑↓〓#&@\^_':;`:¡¢£¦«»¯´·ˊˋƒ‒―‚„†‡•″‹›℅ℓΩ℮↔↕∂─│╒▀▐▪▫◊◦♠♣♥♦⃝⃞‧℧∅∝∞()!*
◢◣◤◥☉♀♂°′〃$£¥‰%℃¤¢⊙●○①⊕◎Θ⊙¤㊣▂ ▃ ▄ ▅ ▆ ▇ █ █ ■ 回 □ 〓≡ ╝╚╔ ╗╬ ═ ╓ ╩ ┠ ┨┯ ┷┏ ┓┗ ┛┳⊥『』┌♀◆◇◣◢◥▲▼△▽⊿
";
}
4、字符去重
再封装一个字符去重的方法,
// 字符去重
public string DeRepeat(string str)
{
StringBuilder sbr = new StringBuilder();
Dictionary<char, object> charDic = new Dictionary<char, object>();
for (int i = 0, len = str.Length; i < len; ++i)
{
if (!charDic.ContainsKey(str[i]))
{
charDic.Add(str[i], null);
sbr.Append(str[i]);
}
}
GameLogger.Log("总字数: " + charDic.Count);
return sbr.ToString();
}
5、执行扫描,自动设置customCharacters
最后,我们调用上面的方法执行扫描,自动设置到字体的customCharacters
字段中,如下
StringBuilder sbr = new StringBuilder();
sbr.Append(ScanPrefabText());
sbr.Append(ScanJsonCfg());
sbr.Append(GetCommonString());
var characters = DeRepeat(sbr.ToString());
var imp = AssetImporter.GetAtPath("Assets/Font/Dengb.ttf") as TrueTypeFontImporter;
imp.customCharacters = characters;
AssetDatabase.ImportAsset("Assets/Font/Dengb.ttf");
七、微信小游戏中文显示问题
好了,啰嗦了那么多,现在回归开头的问题,为什么微信小游戏显示中文的时候出了问题呢?
因为微信小游戏跑的是WebGL
平台,而在WebGL
平台Unity
不能访问系统字库,它忽略Include Font Data
设置,并会始终包含字体数据。所有要用作后备字体的字体都必须包含在项目中,所以如果你用了Arial
动态字体,它在WebGL
平台就只能渲染英文,无法渲染中文字了。
解决办法就是导入一个带中文字库TTF
字体,你可以使用Dynamic
也可以使用Custom Set
,鉴于微信小游戏的内存条件,我建议使用Custom Set
。
另外,关于字体的玩法,我之前写过一篇文章:【游戏开发创新】Unity狗屁不通文章生成器阐述点赞的意义,可生成文字长图保存到本地(Unity | 附源码 | Text转Texture长图 | 详细教程)
里面我讲了如何通过Text
来获取字体信息并把Text
渲染成图片的方法,感兴趣的同学可以看下。
好了,就啰嗦这么多吧~
我是林新发,https://blog.csdn.net/linxinfa
一个在小公司默默奋斗的Unity
开发者,希望可以帮助更多想学Unity
的人,共勉~