In September 2024, War Robots support received a surge of complaints from Chinese iOS players: after updating to iOS 18, in-game text was displaying incorrectly for some users. The issue manifested differently across devices: some players noticed missing characters, while others reported wrong or unfamiliar glyph styles. To make matters worse, we were unable to reproduce the bug on our local test devices.
My name is Sergey Madekin, Senior Developer on War Robots project. In this article, I will share how we ultimately resolved the missing Chinese characters issue.
Investigating the Root Cause
Upon investigation, we discovered that our Chinese text rendering relied directly on system fonts. The game uses standard Unity UI (Text) components configured with dynamic fonts. Unity first searches for required glyphs within the designated font asset, then moves through the Font Names list, and finally falls back to the engine's built-in list of fallback system fonts for the given platform. It was this fallback mechanism that produced inconsistent results across different devices running iOS 18.
A similar issue with missing characters in Legacy Text on iOS 18+ is logged in the Unity Issue Tracker. At the time of writing, no fix version is specified in the ticket.
The First Attempt: Adding the Full Font
The fix itself is straightforward: bundle a dedicated Chinese font into the build and configure it as a fallback for missing glyphs. However, a complete Chinese font file contains tens of thousands of characters. Our base Noto Sans SC TTF file weighed around 10 MB, and adding it increased the final build size by 37 MB.
We initially verified the fix by releasing a build with the full font, keeping in mind that we would optimize it later if it proved effective. The update resolved all text rendering issues with no further complaints, so I moved on to reducing the file size. Fortunately, daily communication requires only a fraction of the full character set, and a specific game requires even fewer.
Optimization and Font Subsetting with pyftsubset
The next challenge was keeping only the necessary characters in the source file. Luckily, the right tool already exists: pyftsubset, a command-line utility from Python's fontTools package designed to create slimmed-down font files containing only specified glyph sets. It discards unused glyphs and metadata, drastically shrinking the file size.
All that remained was identifying which characters to keep. I started with a standard frequency list of common Chinese characters and ran the subsetting tool against it. However, gaps immediately appeared — particularly in localized in-game names, and there were quite a few of them. It became clear that we needed to extract the exact character set used in our text resources. I pulled all current translations and compiled a list of every unique character found in them. I then ran pyftsubset on the source file, keeping only this set. The results were impressive: out of roughly 30,000 characters in the original font, only about 3,000 remained, dropping the TTF size from 10 MB down to around 900 KB. The resulting build size increase was just 1.24 MB.
Build Size Comparison
Edge Cases and Player Nicknames
We shipped the next release with this lightweight font. Player complaints dropped drastically, but a few remained. We discovered that certain configuration files contained unlocalized strings with rare characters. Additionally, some players reported missing characters in their custom usernames.
Because the missing character count was low, I created a dedicated supplementary list. While this resolved all known edge cases, it doesn't guarantee full coverage for future usernames containing obscure characters. Moving forward, whenever a new missing character is reported, we simply append it to the supplementary list and regenerate the font.
Automating the Font Pipeline in Unity
Since game content updates frequently, we needed an easy, automated way to update our character set before each release.
After a few iterations, we built a custom Unity Editor tool that automates the whole process in a single click:
- Connects to the target test server hosting localization files for the upcoming release.
- Downloads the latest text resources.
- Extracts unique characters from the text and merges them with our supplementary list.
- Excludes glyphs already present in the primary game font to avoid duplication.
- Verifies character availability in the source Chinese TTF file. Missing glyphs are logged and skipped.
- (Optional) Includes the character set from the previous release if the "preserve old characters" setting is enabled.
- Runs pyftsubset on the source TTF file using the compiled character list and outputs the optimized font directly into the build.
Preserving the previous release's character set prevents regression bugs if strings are removed from current localization files. While this causes the font size to grow slightly over time, it provides a solid balance between file footprint and backward compatibility.
The Unicode Pitfall: Handling Surrogate Pairs
While refining the generator, we encountered an interesting technical edge case. In C#, strings are UTF-16 encoded, meaning a single char does not always correspond to a complete Unicode character. Rare Chinese characters are represented by two char units—a surrogate pair.
If you extract unique characters using a naive HashSet, surrogate pairs are split. When deduplicating, shared high surrogates can be lost. For example, two distinct rare characters might share the same high surrogate unit: a 4-char string would incorrectly reduce to a 3-element HashSet.
To fix this, we implemented a GetTextScalars method that correctly identifies surrogate pairs and stores each full Unicode scalar as an individual string inside a HashSet:
private static HashSet<string> GetTextScalars(string text)
{
var result = new HashSet<string>();
for (var i = 0; i < text.Length; i++)
{
if (char.IsHighSurrogate(text[i]) && i + 1 < text.Length && char.IsLowSurrogate(text[i + 1]))
{
result.Add(text.Substring(i, 2));
i++;
}
else
{
result.Add(text[i].ToString());
}
}
return result;
}
Here is an example demonstrating the difference:
string text = "\U00020000\U00020001"; // Two distinct characters: 𠀀 and 𠀁
Console.WriteLine(text.Length); // 4 — char count
Console.WriteLine(new HashSet<char>(text).Count); // 3 — unique char units (corrupted)
Console.WriteLine(GetTextScalars(text).Count); // 2 — actual Unicode characters
This Unicode handling edge case is particularly relevant when working with extended Chinese character sets. Adding this check protected our tool against potential data loss, though it was independent of the original iOS system font fallback bug.
By building this custom pipeline, we established a fast, reliable workflow for updating Chinese font assets while keeping our application footprint minimal. The optimized font fully covers our game's localized text, handles custom player names, and maintains character coverage across releases.

Top comments (2)
Not being able to reproduce it on your own test devices is the worst part of font fallback bugs, so nice work tracking it down. A 37 MB jump for a full Noto Sans SC is brutal for a mobile build. I'm curious how you decided which glyphs to keep when subsetting.
We hit the other side of subsetting. The subset was built from one copy of our text files, the game loaded from a second copy, and new strings only landed in one of them. Code points were fine, the glyphs just weren't in the font, so players saw boxes while every text check we had passed. What fixed it was generating the subset in the same build step that writes the strings, plus a check that renders every string and counts missing glyphs.
Do you regenerate the subset on every localization update, or did you keep some headroom of common characters for strings that arrive later?