优化背景:
字体全字符在3万个字符左右,包含了英文字母、汉字和各种语言字符、数字、数学符号、标点符号等。一般一个游戏会用到的字符在最多一千字符左右。比如一个战斗类的游戏,游戏中不会用到乒乓球这三个字,那么字体文件里面就多余了三个字符,所以需要精简。
优化方向:
1、字体文件只包含场景、预制件及语言表中用到的字符,没有多余字符。
2、对于多语言游戏,可针对每个语言制作字体文件,只下载所需语言的字体文件。
3、多字体游戏,可针对每个字体单独统计字符,文本和对应字体管理这一块。
4、聊天功能游戏,聊天文本的字体文件可包含常用8000字。
上面的优化方向是字体字符精简的终点了,没有丝毫多余,有一些劣势也需酌情考虑,例如:语言表只增加了一个字,原来的字体文件需要更新,热更的内容反而变多了,对于经常热更的游戏,一般包含8000个常用汉字和数字字母常用符号即可。
对于重度游戏,字体适度优化即可。对于一般不热更的游戏,或对包体大小比较敏感的小游戏字体优化往往有明显效果。
优化程度:
3万字符——8M
8000字符——2.2M
255字符——430KB
ReplaceTheFont.cs包含字体替换和字符统计功能。
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using UnityEngine.UI;
using System.Text;
public class ReplaceTheFont : EditorWindow
{
private static ReplaceTheFont window = null;
private static List<string> prefafbPathList = new List<string>();
private static Font targetFont;
private static Font curFont;
[MenuItem("Tools/字体工具")]
public static void ShowWindow()
{
if (window == null)
window = GetWindow(typeof(ReplaceTheFont)) as ReplaceTheFont;
window.titleContent = new GUIContent("字体工具");
window.Show();
}
void OnGUI()
{
targetFont = (Font)EditorGUILayout.ObjectField("替换字体:", targetFont, typeof(Font), true);
curFont = (Font)EditorGUILayout.ObjectField("被替换字体(不指定全部替换)", curFont, typeof(Font), true);
if (GUILayout.Button("替换场景中Text的字体"))
{
//寻找Hierarchy面板下所有的Text
var tArray = Resources.FindObjectsOfTypeAll(typeof(Text));
for (int i = 0; i < tArray.Length; i++)
{
Text t = tArray[i] as Text;
//记录对象
Undo.RecordObject(t, t.gameObject.name);
if (curFont != null)
{
if (t.font.name == curFont.name)
{
t.font = targetFont;
}
}
else
{
t.font = targetFont;
}
//设置已改变
EditorUtility.SetDirty(t);
}
Debug.Log("完成");
}
if (GUILayout.Button("替换预制体中Text的字体"))
{
GetFiles(new DirectoryInfo(Application.dataPath), "*.prefab", ref prefafbPathList);
for (int i = 0; i < prefafbPathList.Count; i++)
{
GameObject gameObj = AssetDatabase.LoadAssetAtPath<GameObject>(prefafbPathList[i]);
Text[] t = gameObj.GetComponentsInChildren<Text>();
if (t != null)
{
foreach (Object item in t)
{
Text text = (Text)item;
//记录对象
Undo.RecordObject(text, text.gameObject.name);
if (curFont != null)
{
if (text.font.name == curFont.name)
{
text.font = targetFont;
}
}
else
{
text.font = targetFont;
}
//设置已改变
EditorUtility.SetDirty(item);
}
}
}
AssetDatabase.SaveAssets();
Debug.Log("完成");
}
}
///
/// 获得Asset目录下所有预制体对象
///
///
///
///
public static void GetFiles(DirectoryInfo directory, string pattern, ref List<string> fileList)
{
if (directory != null && directory.Exists && !string.IsNullOrEmpty(pattern))
{
try
{
foreach (FileInfo info in directory.GetFiles(pattern))
{
string path = info.FullName.ToString();
fileList.Add(path.Substring(path.IndexOf("Assets")));
}
}
catch (System.Exception)
{
throw;
}
foreach (DirectoryInfo info in directory.GetDirectories())
{
GetFiles(info, pattern, ref fileList);
}
}
}
[MenuItem("Tools/文本统计")]
public static void GetAllText()
{
StringBuilder SymbolText = new StringBuilder();
StringBuilder Chanesetext = new StringBuilder(",、。.?!~ $ %? ;‥︰…﹐﹒˙?‘’“”〝〞()~〃$¥‰%℃");
//Text数组列表
List<Object[]> tArray = new List<Object[]>();
//寻找Hierarchy面板下所有的Text
tArray.Add(Resources.FindObjectsOfTypeAll(typeof(Text)));
//寻找预制件下的所有Text
GetFiles(new DirectoryInfo(Application.dataPath), "*.prefab", ref prefafbPathList);
for (int i = 0; i < prefafbPathList.Count; i++)
{
GameObject gameObj = AssetDatabase.LoadAssetAtPath<GameObject>(prefafbPathList[i]);
tArray.Add(gameObj.GetComponentsInChildren<Text>());
}
//寻找语言表下所有文字
List<string> language = Language.GetAll();
for (int i = 0; i < language.Count; i++)
{
for (int j = 0; j < language[i].Length; j++)
{
if ((int)language[i][j] > 127)
{
//是汉字
Chanesetext.Append(language[i][j]);
}
else
{
//非汉字
SymbolText.Append(language[i][j]);
}
}
}
//字符全部统计:分为汉字和符号
for (int k = 0; k < tArray.Count; k++)
{
for (int i = 0; i < tArray[k].Length; i++)
{
string text = (tArray[k][i] as Text).text;
for (int j = 0; j < text.Length; j++)
{
if ((int)text[j] > 127)
{
//是汉字
Chanesetext.Append(text[j]);
}
else
{
//非汉字
SymbolText.Append(text[j]);
}
}
}
}
//符号去重
duplicateRemoval(ref SymbolText);
duplicateRemoval(ref Chanesetext);
//可将以下日志打出的字符作为字体文件内容
Debug.Log(SymbolText);
Debug.Log(Chanesetext);
}
private static void duplicateRemoval(ref StringBuilder text)
{
for (int i = text.Length - 1; i > 0; i--)
{
for (int j = 0; j < i; j++)
{
if (text[j] == text[i])
{
text.Remove(i, 1);
break;
}
}
}
}
}
ReplaceTheFont脚本的字体替换和字符统计可以包含四条优化方向,稍加改造即可单独统计每个字体字符,多语言,切换语言统计即可。如果只有一种语言,一种字体,直接使用即可。
实现流程:
1、文本统计:输出两条日志,第一条日志统计了字符,第二条统计了汉字,日志前面加了一些常用标点。将两条日志的字符复制到txt文档,后续会用到该文档。
2、FontSubsetPack
使用FontSubsetPack生成精简后的字体文件。
FontSubsetPack插件,FontCreator字体格式转换工具百度可查。FontSubsetPack只能用ttf字体,可以先将OTF字体转换为ttf字体。