Введение

Всем привет! Меня зовут Александр и я 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)


  1. DmitriySun
    13.07.2024 12:47
    +1

    А что будет если, я в китае, смартфон китайский, но я хочу играть на русском?


    1. cimiox Автор
      13.07.2024 12:47

      Хороший вопрос! Все зависит какой язык стоит у игрока в ОС, если китайский, тогда игра будет на нем и придется делать возможность переключения языка.

      Но тут уже зависит как вы реализуете локализацию. Fallback шрифты уже сработают по порядку, кто первый отдаст символ тот и отобразится. Надеюсь ответил на вопрос.


  1. mynameco
    13.07.2024 12:47

    В бандлы нужно паковать шрифт и качать тот что нужен.


    1. cimiox Автор
      13.07.2024 12:47

      Конечно, так можно и нужно делать, но при условии, чтобы бандлы не хранились в билде. В лучшем случае, те ~25 МБ о которых говорится в статье, можно сжать до 10 МБ, что так же много для мобильной игры.


  1. mopsicus
    13.07.2024 12:47
    +1

    Хороший проект!

    А что насчет эмоджи? Есть возможность использовать родные эмоджи от платформы?


    1. cimiox Автор
      13.07.2024 12:47
      +1

      Отличный вопрос!
      Эмоджи не пробовал грузить (такой задачи пока что не было), но знаю что в iOS есть такой шрифт с расширением ttc и весит он не мало, называется AppleColorEmoji-160px (гляньте в списки шрифтов).

      Думаю через шрифты такое не получится сделать, но через TMP_SpriteAsset имеет смысл попробовать! Хорошая идея продолжить статью:)


      1. mopsicus
        13.07.2024 12:47

        Да, тема актуальная для тех кто делает различные игры и аппы где есть пользовательский ввод. Сейчас эмоджи так и сделаны, есть несколько атласов с подготовленными спрайтами и оттуда они подставляются в текст. Но если можно использовать родные, то вес приложения уменьшится конечно.