【游戏开发解答】Unity发布微信小游戏,中文字无法显示的问题(自制字体库 | Font | Custom set | 动态字体 | 静态字体)

2023-05-16

文章目录

      • 一、前言
      • 二、Unity默认的字体:Arial
      • 三、查看动态字体的动态纹理
      • 四、用动态字体还是静态字体
        • 1、用不用Arial动态字体
        • 2、用Dynamic还是Custom set
      • 五、字体文件资源瘦身
        • 1、办法一:字体文件本身做裁剪
        • 2、办法二:制作静态字体
      • 六、扫描工程中用到的字,自动设置Custom Chars
        • 1、配置表、代码文本扫描
        • 2、预设文本扫描
        • 3、通用标点符号、数字、字母
        • 4、字符去重
        • 5、执行扫描,自动设置customCharacters
      • 七、微信小游戏中文显示问题

一、前言

嗨,大家好,我是林新发。
我使用Unity开发项目,然后发布成微信小游戏的时候遇到了一个问题:有部分中文字无法显示。
发现是因为个别界面预设中的Text使用了默认的Arial字体,为什么会出现中文字无法显示的问题呢?
我觉得有必要写篇文章讲讲。

注:可能有同学会问:Unity开发的项目可以直接发布微信小游戏吗?答案是可以滴,不过需要安装一个发布工具插件,插件GitHub地址:https://github.com/wechat-miniprogram/minigame-unity-webgl-transform
详细参见GitHub中的文档即可,如果有需要的话我再另外写文章讲解操作流程。

二、Unity默认的字体:Arial

Unity默认使用的字体是Arial,比如你用UGUI创建一个Text,你就会看到它使用的FontArial
在这里插入图片描述
ArialWindows的系统字体之一,Unity直接访问它,并以动态字体Dynamic)的形式渲染。
Arial本身并不包含中文字库,为了证明Arial字体不包含中文字库,我使用FontCreator这个软件打开arial.ttf字体,然后预览窗口中输入Hello,我叫林新发,可以看到,中文字显示不了:
在这里插入图片描述

注:FontCreator这个软件可以从我的GitCode上免费下载:https://gitcode.net/linxinfa/fontmaker/-/blob/master/FontCreator.zip?from_codechina=yes
另外,我之前写过一篇关于字体裁剪的文章,里面有介绍这个软件的使用:《字体裁剪,精简字体,字体瘦身:FontSubsetGUI,FontCreator,FontPruner》

Unity是怎么把中文显示出来的呢?我贴一段Unity官方手册的说明吧~

Unity官方手册
Unity尝试使用 动态字体 渲染文本但无法找到字体时(因为未选择 Include Font Data__,并且用户计算机上未安装该字体),或者字体不包含请求的字形时(例如尝试使用拉丁字体在东亚脚本中渲染文本时,或者使用粗体/斜体字形文本时),它将尝试Font Names__字段中列出的每种字体,从而查看是否可以找到与项目中的字体名称匹配的字体(包含字体数据)或者用户计算机上安装的字体是否具有请求的字形。如果找不到列出的后备字体或者这些字体不具有所请求的字形,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节点上,并赋值TextRawImage对象,如下,
在这里插入图片描述
运行Unity,现在可以看清字体的纹理啦,
在这里插入图片描述
现在我们来玩个好玩的,我动态调整Text的字号,可以看到它动态生成了不同字号的纹理,如下
请添加图片描述
这就是为什么我们使用动态字体时,不同字号的字清晰度不同的原因,它会根据你字号所在的段位查找匹配的纹理进行渲染。

四、用动态字体还是静态字体

用动态字体还是静态字体,这个问题要具体情况具体分析。

1、用不用Arial动态字体

如果你的项目是纯英文的项目,你可以使用默认的Arial动态字体,否则不要使用默认的Arial动态字体,原因如下:

不同平台的系统默认字体不同,比如在Android系统中Unity3D默认会去查找名为DroidSansFallback的字体,但是因为Android系统的可定制性,很多手机厂商会去修改默认字体。Android系统是根据字体的文件名称来找字体的,但Unity3D识别字体却是通过字体内部的设置来识别字体的,所以即使在Android手机中有DroidSansFallback.ttf字体,也有可能导致Unity3D找不到这个字体。由于上面的原因,尽量不要使用Unity3D的默认字体Arial,除非你做的游戏是纯英文的。

2、用Dynamic还是Custom set

自己导入一个TTF字体,是用Dynamic还是用Custom set呢?
在这里插入图片描述
我们先来做个实验,以这个这个Dengb.ttf字体为例,它有15M这么大(实际项目不会用这么大的字体,会做一些处理,下文会讲解决办法),
在这里插入图片描述
假设我们使用Dynamic,并且勾选的了Incl. Font Data,如下,
在这里插入图片描述
现在我们使用Addressables系统,把它单独放在一个Group中,如下

注:关于Addressables系统,我之前写过一篇教程,可以参见我之前的这篇文章:《【游戏开发探究】Unity Addressables资源管理方式用起来太爽了,资源打包、加载、热更变得如此轻松(Addressable Asset System | 简称AA)》

在这里插入图片描述
现在们打包资源,看生成的.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 Size60,即使这样,生成的纹理尺寸已经达到极限的 4096 x 4096,有16M这么大,
在这里插入图片描述
可以酌情得把常用汉字减少一些,比如减到5000字,另外,如果你非常确定所需要显示的字量,比如我非常明确只用到了Hello,林新发这几个字,Custom Chars中我就只需要填Hello,林新发这几个字,
在这里插入图片描述
生成的纹理只有32KB
在这里插入图片描述
我们运行时动态修改Text的字号,可以发现它始终都是引用静态字体的纹理,不会像动态字体那样动态生成纹理,字号的调整仅仅只是做纹理的缩放,当缩放过大时就会显得模糊,
请添加图片描述
另外,由于纹理是静态的,TextFont 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的人,共勉~

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【游戏开发解答】Unity发布微信小游戏,中文字无法显示的问题(自制字体库 | Font | Custom set | 动态字体 | 静态字体) 的相关文章

  • Linux命令行初接触-1 操作文件和目录

    操作文件 amp 目录 1 通配符含义常用通配符常用字符类类型匹配范例 2 mkdir 创建目录3 cp 复制文件和目录工作方式常用选项 4 mv 移动和重命名文件工作方式常用选项 5 rm 删除文件和目录工作方式常用选项注意事项 6 ln
  • 机器学习入入入入门(1)机器学习基本概念、引出深度学习

    机器学习入入入入门 xff08 1 xff09 0 前言1 基本步骤2 基本概念2 1 Hyperparameters2 2 local minima 3 linear model3 1 基础概念 4 piecewise linear cu
  • 深度学习蒟蒻入门——从0安装pytorch(CPU版)

    从0安装pytorch 1 检查自己的电脑有没有GPU2 安装CPU版的pytorch3 测试pytorch 1 检查自己的电脑有没有GPU 首先打开任务管理器 xff0c 选择性能栏 然后滑到最下 xff0c 看是否有GPU一项 xff0
  • 系统学习iOS动画 —— Stroke和路径动画

    这是要完成的动画 xff1a 先添加需要的代码 xff0c 这里需要将storyboard的ViewController换成TableViewController xff0c 将Under Top Bars 和 Under Bottom B
  • 不知道这些网站还做什么程序员呀!

    今天我就来总结一些程序员必备的网站 xff0c 囊括开源项目 解决bug 技术分享 一线资源和自我提升的网站 xff0c 希望能对广大程序猿有所帮助 xff0c 赶紧给我收藏起来 xff0c 下次刷不到了可别说我没提醒你 我们首先来看一下国
  • (音视频开发)WebRTC进阶流媒体服务器开发-多人互动架构

    一 xff1a 多人互动架构方案 xff08 一 xff09 WebRTC回顾 xff0c 两层含义 xff1a 1 WebRTC是google开源的流媒体客户端 xff0c 可以进行实时通讯 xff0c 主要应用于浏览器之间进行实时通讯
  • 10种linux下磁盘快照方式恢复系统

    导读大家都知道windows系统有一个磁盘快照的功能 xff0c 在windows2003中系统恢复开始依赖于一个叫做硬盘快照服务 Volume Snapshot Service 的服务 xff0c 他能够自动创建系统快照 包括正在使用的文
  • ubuntu安装go开发环境

    一 为ubuntu20 04更新源 给root用户设置密码 xff1a 命令 xff1a sudo passwd root 备份原来的源 xff0c 命令 xff1a sudo cp etc apt sources list etc apt
  • 如何修复Ubuntu中包缓存文件被毁问题

    导读今天 xff0c 我尝试更新我的 Ubuntu 18 04 LTS 的仓库列表 xff0c 但收到了一条错误消息 xff1a E The package cache file is corrupted it has the wrong
  • 1002 A+B for Polynomials (25分) 详解+易错点

    注意点 xff1a 系数为0 xff0c 则不输出 xff0c 例 xff1a 其中 1和1相加为0 xff0c 则在输出时避免这一项 xff0c 而且要注意结果的K值 xff0c 不要包括这一项 xff0c 思路 xff0c 利用结构体存
  • Linux远程桌面的选择

    Linux的远程桌面主要分两个部分 xff1a Linux客户机连Linux服务器和Windows客户机连Linux服务器 xff0c 还有现在用平板电脑连远程桌面 Linux客户机连Windows服务器比较简单没啥可说的 xff0c rd
  • Kali Linux mdk3WiFi洪水攻击 攻击路由器 生成虚假WiFi WiFi身份验证攻击可使连接WiFi的手机掉线重连抓包

    将无线网卡转换为监听模式 airmon ng start wlan0 查找附近无线网络 airodump ng wlan0mon Authentication DoS xff1a xff08 洪水攻击 xff0c 又叫做身份验证攻击 xff
  • 大一java程序设计的某次作业题解

    题目描述 xff1a 设计程序实现输入日期及机票张数 xff0c 计算出应付金额 假设北京至上海的机票全价为 1200 元 张 xff0c 以 2017 年为例进行程序编写 xff0c 所有的法定假日 xff0c 机票无折扣 xff1b 除
  • D. Make It Round(1759D)

    要求n k后缀0数量最多 xff08 k lt 61 m xff09 xff0c 且n k尽可能大 比赛时思路 xff08 错误 xff09 xff1a 10是由2和5组成 xff0c 故先统计n的因子包含2的个数num2 包含5的个数nu
  • Codeforces Round #841 (Div. 2)

    B Kill Demodogs 分析 显然要选择和两斜线的元素相加 所以答案可以表示为 xff1a 即 xff1a 根据公式 得答案为 但答案不能直接得这个 因为n的范围为1e9 xff0c 而ull的范围为1e20 xff0c 答案的第一
  • Educational Codeforces Round 141 Editorial C~D

    1783C Yet Another Tournament 分析 正解思路是贪心 开始自己也想的贪心 xff1a 首先显然打败的人数越多越好 xff0c 然后选择权值最小的人打败 这个思路前半部分没问题 xff0c 后半部分过不了样例的第二个
  • Codeforces Round #844 (Div. 1 + Div. 2, based on VK Cup 2022 - Elimination Round) D

    1781D Many Perfect Squares 分析 对于每组 xff0c 若和均为完全平方数 xff0c 则存在 xff1a 所以枚举所有 xff0c 对于每个 xff0c 枚举其所有 双因子对 xff0c 若两个因子之差为偶数 x
  • 匹配已有字符串

    生活小妙招 通过set和substr函数 xff0c 方便快捷地写出匹配已有字符串的代码 前置芝士 xff1a set使用详解 题目 xff1a G Perfect Word 代码实现 通过set 43 string的substr的使用快速
  • 在vue中使用rules的定义和校验规则

    表单内容里面定义属性 lt Form ref 61 34 rulesForm 34 model 61 34 rulesForm 34 label width 61 34 100 34 rules 61 34 rules 34 gt lt F
  • Codeforces Raif Round 1 (Div. 1 + Div. 2) D

    D Bouncing Boomerangs 分析 一个stack用于存只有一个的物品的行的物品位置 xff0c 一个queue用于存有两个物品的行的左边物品位置 xff08 其实这两个容器用stack还是queue无所谓 xff0c 只是这

随机推荐