Introduction
Last summer (the summer of 2023), I was contracted by Lemonade Flashbang to write a patch for the Godot game engine. The patch fixes text display issues in the simplified Chinese, traditional Chinese, and Japanese localizations of Doomsday Paradise. By patching Godot, Lemonade Flashbang could present its game at Bitsummit last July and launch it on Steam last November.
The Problem
Before version 4.0, Godot produced inappropriate line-breaking behavior for CJK text. The inappropriate behavior was
observable using both the Label
and
RichTextLabel
nodes. Unfortunately, in
Godot 3.x, automatic line-breaking is handled differently in Label
and RichTextLabel
nodes. Each node requires a
separate solution (or a large-scale change to its functionality, such as the changes included in 4.0). Label
nodes
break the entire text into a linked list of separable strings. When inserting more text into the list would overflow a
line, Godot inserts a specialized
WRAPLINE
element. RichTextLabel
nodes,
on the other hand, use a more complex approach. A Godot 3.x RichTextLabel
node contains a linked list of
RichTextLabel::Item
objects.
Each Item
represents a formatted segment of the RichTextLabel
node’s content. However, not all of the content of a
RichTextLabel
needs to be text. The result is that each Item
may represent the entire text (the simplest case), a
portion of the text, or no text at all. When automatically inserting line breaks into a RichTextLabel
, Godot 3.x
only considers whether or not the text in the current Item
would overflow the current line. This approach works fine
for English text but is a source of many errors for languages using CJK characters.
To make matters more complex, Godot 3.x’s source code exhibits some common issues often found in lower-quality
C++ source code. In both Label
and RichTextLabel
nodes, the method that handles the
automatic line-breaking process is quite long.
RichTextLabel::_process_line
(the method responsible for automatic line-breaking in RichTextLabel
nodes) is over 700 lines long.
RichTextLabel::_process_line
(the primary offender) also includes several extensive macro definitions. These macros,
such as NEW_LINE
, reduce
the size and redundancy of an already giant method implementation. However, they also defy convenient debugging with
my debugger (GDB). As a result, I had to trace these sections by hand when
necessary. The source code also contains many short (often one or two-character) symbol names. There’s a lot of
C++ source code with inappropriately vague symbol names, so this isn’t a shocker. Just
like everything else I’ve mentioned, though, this makes the source code more difficult to read and, ultimately, to
debug.
The Solution
To solve these problems, the patched version of Godot
had to change both the Label
and RichTextLabel
nodes. The core of both nodes’ new CJK line-breaking functionality
is the gnomesort::is_cjk_x_char
family of
functions. These functions provide a modular way for Godot scene nodes to classify CJK text. This functionality
includes simple detection (i.e., is the text CJK or not), detection of whether or not a character may end a line,
detection of whether or not a character may begin a line, and detection of whether or not a character is separable
from its neighbors.
The separability question is particularly complex because it requires analyzing the characters before and after the
current character (if they exist). Determining separability is no big deal for Label
nodes because they always hold
their entire text. RichTextLabel
nodes are another story. For RichTextLabel
nodes, Godot needs to traverse the
internal Item
list. Besides the additional complexity of linked list traversal, which is minimal, RichTextLabel
nodes require analyzing several characters ahead of the current character to determine the locations of safe breaking
points. This extra complexity is owed, in no small part, to the generally messier way that RichTextLabel
nodes are
processed. Even with additional context, there are still cases where RichTextLabel
nodes can produce incorrect
results.
The final major issue arises when a RichTextLabel
mixes CJK text with Latin text. For example, this can occur when
text is written in Japanese but switches to Latin characters for a name (likely from user input). Initially, I chose
to detect which rules applied to a given text Item
within a RichTextLabel
by scanning the Item
entirely. This
strategy often works because the entire segment should use the CJK rules if there is CJK text. Problems arise when a
switch occurs across a formatting boundary. This issue can occur when text, like the name in the previous example, is
colored or has some other special formatting (e.g., bold, italics, underlining, etc.). In the name of expediency, I
ultimately chose to implement a toggleable setting that controls whether a RichTextLabel
should apply the CJK rules
to all text.
Thoughts
At the end of the day, I stand by the admittedly expedient solution I arrived at. It’s rougher around the edges than I
would like, but that kept time and costs to a minimum. Better solutions exist; switching to Godot 4.0 and relying on
TextServer
is the most obvious example, but
it wouldn’t have been the correct answer for the client. Still, the time-constrained nature of these types of
contracts frustrates my perfectionist tendencies.
Besides that, I wonder how using a framework like Godot impacts the economics of a project like Doomsday Paradise. The simple analysis says it saves time and money, but this project indicates that the simple analysis of large software frameworks is only sometimes correct. Other times, when the framework doesn’t cover all of a developer’s needs perfectly, developers will have to expend resources on workarounds. How often is that the case, and how much do these workarounds cost? Unfortunately, I don’t have an answer, and I haven’t been able to find much rigorous data on the subject. My intuition is that the costs will likely be higher than they initially seem.
The only way for me to find out is to pursue more of this type of work in the future. Working with Lemonade Flashbang was my first experience with a commercial game project. It felt like a big step for me despite my relatively small contribution. Before this, I had tried to work on a few of what I would describe as amateur game projects. Unfortunately, those never really went anywhere. I still want to try for more amateur projects. However, I enjoyed getting into Godot’s guts, and I know amateurs tend to flee the moment anyone mentions C++. Either way, I’d like to take on more work like this sooner than later.