Введение
Всем привет! Меня зовут Александр и я Unity Developer более 7 лет. В этой статье мы попробуем решить проблему шрифтов раз и навсегда (в мобильных играх так точно). Способ для Unity не самый очевидный, про него не так много написано и все ответы приходилось собирать по кусочкам, собственно поэтому и решил написать статью. Сразу перейдем к техническому заданию.
Нужно сделать локализацию для мобильной игры, с такими требованиями:
Шрифты не должны занимать много места в билде, желательно до 10 мб максимум
Шрифты должны быть сгенерированы без лишних заморочек для Text Mesh Pro
Шрифты должны поддерживать такие языки: English, Russian, Ukrainian, German, French, Spanish, German, Italian, Portuguese, Arabic, Japanese, Chinese, Korean, Hindi
Проблема
Итак, давайте разбираться в самой задаче по порядку. Первый пункт про размер шрифта: если подбирать шрифт для европейских языков, то такой шрифт будет весить обычно не больше 1 мб. Но чем ближе мы будем двигаться в сторону Азии, тем больше будет размер шрифта (интересное сходство?). В итоге получается примерно 24.6 мегабайт, что не мало для мобильной игры. Стоит учесть, что некоторые шрифты взяты в единственном стиле, иначе размер увеличивается практически вдвое.
Language |
Font |
Size |
English, Russian, Ukrainian, German, French, Spanish, German, Italian, Portuguese |
Noto Sans |
0.6 MB |
Arabic |
Noto Sans Arabic |
0.7 MB |
Hindi |
Noto Sans Devanagari |
0.9 MB |
Japanese |
Noto Sans Japanese Black |
5.7 MB |
Korean |
Noto Sans Korean Black |
6.2 MB |
Chinese |
Noto Sans Chinese Simplified |
10.5 MB |
Итого: |
24.6 MB |
Второй пункт так же не простой из-за не знания всех языков, при генерации TMP шрифта можно упустить какие-то символы. Например для китайского языка есть 3500 часто используемых иероглифов из 20000 только официального словаря. С третьим чуть полегче, нам помогут дизайнеры найти нужные шрифты, и наше дело их добавить в игру.
Решение проблемы
После долгого поиска шрифтов и осознания, что они будут занимать 1/3 размера игры, появилась идея: “В операционных системах же есть уже встроенные шрифты для разных языков, почему бы их не использовать”. Потратив некоторое время на поиски, я наткнулся на форум, где уже этот вопрос обсуждался с разработчиком TMP. В треде добились чтобы разработчик сделал такой функционал. Хоть функционал есть, но шрифты, о которых пишет Apple и Google, находятся не все или названы совсем не так. Тут можно найти списки шрифтов для мобильных платформ
Допустим, мы нашли нужные нам шрифты, теперь давайте их подключим. Нам понадобиться какой-то основной шрифт, скорее всего это будет шрифт Английского язык, его мы заранее создадим в нашем проекте (туториал). Дальше загрузим нужные нам шрифты из ОС:
private static IEnumerable<(NativeFontData data, string path)> GetNativeFonts(IEnumerable<NativeFontData> data)
{
var nativeFonts = Font.GetPathsToOSFonts();
foreach (var fontData in data)
{
var currentFontName = fontData.Name;
var nativeFont = nativeFonts.FirstOrDefault(nf => IsTargetFont(currentFontName, nf));
if (nativeFont is not null)
{
yield return (fontData, nativeFont);
}
else
{
Debug.Log($"Can't find any font with name: {fontData.Name}");
}
}
yield break;
bool IsTargetFont(string target, string current)
{
return target == Path.GetFileNameWithoutExtension(current);
}
}
Теперь нам нужно эти шрифты конвертировать в TMP и настроить. При настройке шрифта нужен индивидуальный подход для каждого, так как Японский шрифт с теми же настройками как у Итальянского может хуже читаться или занимать больше места в памяти.
private static IEnumerable<TMP_FontAsset> ConvertNativeFontsToTmp(IEnumerable<(NativeFontData data, string fontPath)> fontPaths)
{
foreach (var (data, fontPath) in fontPaths)
{
var font = new Font(fontPath);
var tmpFontAsset = TMP_FontAsset.CreateFontAsset(font, data.PointSize, data.Padding,
GlyphRenderMode.SDFAA_HINTED,
data.AtlasSize.x, data.AtlasSize.y);
yield return tmpFontAsset;
}
}
Чуть подробнее опишу некоторые конфигурации у шрифта:
Point Size - качество рендеринга символа, выше лучше (хороший показатель 40-50)
Padding - расстояние между символами в атласе
Atlas Size - размер каждого создаваемого атлас
Dynamic Font - каждый символ, перед отображением, будет пытаться найти такой же в шрифте и после добавит в атлас
Multi Atlas Textures - если заканчивается место для символов создастся новый атлас, иначе не будет добавлен символ
Полный код можно найти здесь. Как видно из кода, в функции есть аргумент текущего языка платформы, в моем случае мы используем только 1 язык без возможности изменения его во время игры. В случае, когда понадобятся все поддерживаемые языки (например для чатов), необходимо немного усовершенствовать функцию. Здесь стоит быть осторожным: память на устройстве не резиновая, и стоит контролировать количество и размеры атласов шрифтов (опять же отсылка к Китайскому языку). Так же я добавил пример настроек для разных платформ, которыми сам пользуюсь.
var fontData = Application.platform switch
{
RuntimePlatform.Android => new[]
{
//Android
new NativeFontData("NotoNaskhArabic-Regular"), //Arabic
new NativeFontData("NotoSansDevanagari-Regular"), //Hindi
new NativeFontData("NotoSansCJK-Regular", 40, 3, new Vector2Int(2048, 2048)), //Chinese, Japanese, Korean
new NativeFontData("Roboto-Black"), //Unicode
new NativeFontData("Arial"), new NativeFontData("LiberationSans")
},
RuntimePlatform.IPhonePlayer => new[]
{
//iOS
new NativeFontData("HiraginoMincho"), //Japanese
new NativeFontData("NotoNastaliq"), //Arabic
new NativeFontData("DevanagariSangamMN"), //Hindi
new NativeFontData("PingFang", 40, 3, new Vector2Int(2048, 2048)), //Chinese
new NativeFontData("AppleSDGothicNeo"), //Korean
new NativeFontData("SFUI"), //Currency symbols
new NativeFontData("Arial"), new NativeFontData("LiberationSans")
},
_ => new[] {new NativeFontData("Arial"), new NativeFontData("LiberationSans")}
};
Заключение
В финальном результате получилось оптимизировать билд, процессы поиска и добавления шрифтов. Вот небольшой список достижений:
В проекте только один шрифт
Уменьшился размер всех шрифтов до 500 кб
Теперь нет необходимости конвертации шрифта в TMP
Поддержка практически любых языков
P.S. Еще раз продублирую ссылку на репозиторий. Если понадобится сделать из этого пакет - напишите об этом в комментах.
Комментарии (7)
mynameco
13.07.2024 12:47В бандлы нужно паковать шрифт и качать тот что нужен.
cimiox Автор
13.07.2024 12:47Конечно, так можно и нужно делать, но при условии, чтобы бандлы не хранились в билде. В лучшем случае, те ~25 МБ о которых говорится в статье, можно сжать до 10 МБ, что так же много для мобильной игры.
mopsicus
13.07.2024 12:47+1Хороший проект!
А что насчет эмоджи? Есть возможность использовать родные эмоджи от платформы?
cimiox Автор
13.07.2024 12:47+1Отличный вопрос!
Эмоджи не пробовал грузить (такой задачи пока что не было), но знаю что в iOS есть такой шрифт с расширением ttc и весит он не мало, называется AppleColorEmoji-160px (гляньте в списки шрифтов).
Думаю через шрифты такое не получится сделать, но через TMP_SpriteAsset имеет смысл попробовать! Хорошая идея продолжить статью:)mopsicus
13.07.2024 12:47Да, тема актуальная для тех кто делает различные игры и аппы где есть пользовательский ввод. Сейчас эмоджи так и сделаны, есть несколько атласов с подготовленными спрайтами и оттуда они подставляются в текст. Но если можно использовать родные, то вес приложения уменьшится конечно.
DmitriySun
А что будет если, я в китае, смартфон китайский, но я хочу играть на русском?
cimiox Автор
Хороший вопрос! Все зависит какой язык стоит у игрока в ОС, если китайский, тогда игра будет на нем и придется делать возможность переключения языка.
Но тут уже зависит как вы реализуете локализацию. Fallback шрифты уже сработают по порядку, кто первый отдаст символ тот и отобразится. Надеюсь ответил на вопрос.