← 所有文章

像素艺术界面:文本框、字体与日语

《宝可梦 绿宝石》的对话框是一个横30格、竖6格图块的边框,占满屏幕240像素的整个宽度和30%的高度,框内是27×4图块的文本窗口,以16像素字体容纳两行;《宝可梦 水晶》的对话框是20×6图块,两行,每行18个字符。123 《绿宝石》按速度设置每8、4或1帧显示一个字符,在60赫兹下即每秒7.5、15或60个;《水晶》的最快设置和《最终幻想VI》同样每帧显示一个。14 《绿宝石》的编剧逐行手动换行:在24,100行剧本文字中,最宽的一行为208像素,窗口为216像素,没有一行溢出。1 覆盖标准字符集的日语像素字体要画大约七千个字符:JIS X 0208有6,879个,Misaki在8×8的单元格里画齐了全部,PixelMplus10在10像素网格上画齐了除三个码位之外的全部,《最终幻想 像素复刻版》的制作人说团队新做的日语字体有“around 7,500 characters”(大约7,500个字符)。5678 在我的Mac上关闭字体平滑后,Core Text对网格位于基线上、步进为整数像素的像素TrueType字体(Misaki、PixelMplus10和Press Start 2P),在网格的整数倍下绘制时没有任何中间调像素,而在整数倍之间,有19.4%到82.2%的着墨像素发虚;网格偏离基线的PixelMplus12、轮廓上有离群点的Silkscreen、以及没有网格的DotGothic16,在测量的每个尺寸下都出现中间调,k8x12在字形被精确放置后也在每个尺寸下出现中间调;而在绘图上下文的默认平滑下,即便PixelMplus10以其本身的10像素绘制,272个着墨像素中也有178个是中间调。79 在3×屏幕上,10像素的em只在iOS十二档Body尺寸中的一档,即40点,落在整数设备像素上。710 如今的Kiradex World用系统等宽字体排每一个字,只说英语,给VoiceOver的是行走测试用的坐标。11 于是我从反编译中测量了经典作品的对话框、字体和速度,在Core Text上测量了五款日语和两款拉丁字母像素字体,并写了一份简报:两种语言共用一款有授权的字体,按字体网格取整的Dynamic Type,由锻炉绘制的边框,两行加翻页,以及一个VoiceOver能够描述的舞台。

TL;DR

  • 对话框占屏幕下方三分之一,两行。 《绿宝石》的对话边框为30×6图块(240×48像素,屏幕高度的30%),框内是容纳两行16像素文字的27×4图块窗口;《水晶》的对话框为20×6图块,占高度的33%,两行,每行18个字符,《红》也是同样的20×6。《绿宝石》的对话框没有名字栏,其剧本14,412个段落中有934个(6.5%)在正文里写出说话人的名字。12312
  • 速度是帧数。 《绿宝石》和《火红》每个字符用8、4或1帧(60赫兹下每秒7.5、15或60个),在显示过程中按过一次A或B之后,按住期间每帧一个字符;《水晶》和《红》等待5、3或1帧(每秒12、20或60个);《最终幻想VI》以每帧一个字母的速度绘制场景对话,且没有相应设置(其六档信息速度是战斗信息的等待时长)。《绿宝石》的翻页箭头从静止位置依次移动0、1、2、1像素,每9帧一步,一个周期0.6秒;《水晶》的▼与边框图块交替闪烁。1131415164
  • 游戏需要的日语,已有五款免费字体画好了。 Misaki以8×8画齐JIS X 0208全部6,879个字符,每个汉字都在7×7之内;k8x12以8×12画齐;PixelMplus12和PixelMplus10以12和10像素画齐除三个码位外的全部;DotGothic16全部画齐,但不在任何像素网格上。Misaki、k8x12和PixelMplus共用的三句话授权允许使用、复制和分发,“either commercially or noncommercially”(无论商业或非商业)。5671718
  • 像素字体只有在一个字体像素等于整数个设备像素时才清晰,而且字体文件本身必须干净。 测量用的是macOS 27.0上的Core Text,不是iPhone,并关闭了字体平滑:网格位于基线上、步进为整数像素的字体(Misaki、PixelMplus10、Press Start 2P),在网格整数倍下中间调像素为0.0%,在整数倍之间为19.4%到82.2%。7 在绘图上下文的默认平滑下,PixelMplus10以其本身的10像素绘制时,272个着墨像素中有178个是中间调,因此“整数倍即干净”的结果只在关闭平滑时成立。9 PixelMplus12的网格位于基线下方0.39像素,即使关闭平滑,也在每个尺寸下发虚;k8x12的假名步进为8.01像素,字形被精确放置后就会发虚。719
  • Dynamic Type必须取整,而不是照搬。 iOS的十二档Body尺寸(14到53点)中,在3×屏幕上,10像素的em只在40点时为整数,8像素的em在16和40点,12像素的em在16、28和40点。Apple要求自定义字体实现系统字体所具备的行为,Dynamic Type即在其中;对于应用的文字,则要求理想情况下让人至少能放大到200%。71020
  • 日语换行:不加标签时严格,加标签后宽松。 在macOS的Core Text中,一个131字符的段落在不加标签时,无论测量的哪个宽度,都没有一行以探针行首列表中的字符开头;这份列表并不完整,大部分是JLREQ的字符,其中七个是全角形式。加上ja标签后,在Hiragino Sans、Misaki、PixelMplus和DotGothic16中,7个宽度里有6个出现了小写假名位于行首,也就是W3C日语排版需求所描述的较宽松规则。721
  • 这对Kiradex意味着什么。 是一份简报,不是成品:世界之上绘制的每个字都用PixelMplus10(在测量过的、同时具备日语、干净网格和整数像素步进的两款字体Misaki和PixelMplus10中,较大的那款);尺寸取整到每个字体像素为整数个设备像素(3× iPhone默认尺寸下为16.67点,2×下为15点);锻炉自绘的边框;两行加翻页;每秒30个字符的逐字显示和一个分步移动的标记;单行气泡,其宽度在左右无边距时能放进iOS 27.0模拟器列出的最窄iPhone;放不下的台词转入文本框;日语台词由日语母语者审阅;以及一个由VoiceOver用文字而非坐标来描述的舞台。每一项都配有一条可由脚本、一行日志或一张截图判定通过或失败的检查,只有一项VoiceOver检查是手动的。2211

1. 《绿宝石》与《火红》:占满屏宽的边框、两行、手工换行

这个系列从世界开始,一路向内推进。iPhone上的像素艺术世界确立了图块系统和RealityKit配方,对文字只给出一条规则“Text is art”(文字即美术)和一句计划:“m5x7 at integer multiples in a 24-by-24 nine-slice box, two lines, typewriter reveal, a two-frame advance arrow.”(m5x7按整数倍,放在24×24的九宫格框里,两行,打字机式显示,两帧的翻页箭头)。23 之后的文章做出了收藏者、建筑、运动、小镇自身的生活、房间和道路。文字则一直停留在那一句话上。本文测量那些数字从何而来,找出世界篇中四处错误或不精确的地方,并补上那篇没有的内容:8、10和12点阵的日语,Core Text实际上如何处理像素TrueType字体,Dynamic Type与像素网格怎样才能协调,日语换行,以及RealityKit舞台上的VoiceOver。

Kiradex World目前的文字很朴素。每个字都是SwiftUI,用主题的系统等宽字体。收藏者头顶的气泡是套件caption角色的一行粗体,奶油色纸面配2点的墨色描边,在台词出现后不满4秒期间显示。地点的那行文字放在屏幕底部的奶油色文本框里。应用只有英语,预设台词以英语存放在服务器上,锻炉画的招牌是一块12×11的木板,上面只有墨迹涂画,没有文字,而VoiceOver把舞台读作一个元素,其值是行走测试的步伐记录。11242526272829 详情见第10节;第12节的简报正是以与这一现状的差异来写的。

下面所有宝可梦的数字,都是用一个Python脚本measure_gen_text.py,从pret反编译的浅克隆pokeemerald 731ad5b(2026年10月1日)、pokefirered 037335f、pokecrystal 5beda23和pokered d2704a6中测得的,输出保存在脚本旁边;《最终幻想VI》的数字来自针对everything8215/ff6反汇编的第二个脚本。两者都能在几秒内重新运行,脚注写明了每个数字背后的源文件。3014

字体

《绿宝石》在一张表中声明它的字体,即src/text.c里的sFontInfos,每项都以像素为单位写明字母的最大宽度和高度。小字体为5×12;对话框使用的常规字体为6×16;短字体为6×14,声明了四次(本身加三个副本);窄字体为5×16;小窄字体为5×8;粗体字体为8×8;盲文字体为8×16,行距8像素。其余字体的行距都是0:16像素的单元格就是一行。113

常规拉丁字母字体是比例字体。通过游戏的charmap.txt查src/fonts.c中的gFontNormalLatinGlyphWidths,小写字母a到z平均宽5.77像素(4到6);大写字母A到Z和数字0到9都是6;空格和句点是3,感叹号是4。131 在实际文字中,以下文游戏剧本的每一行来测,平均每个字符为5.25像素。1

速度是帧数

《绿宝石》的文字速度是以帧计的延迟。选项画面的三档速度通过src/menu.c中的sTextSpeedFrameDelays对应到延迟8(慢)、4(中)和1(快)。src/text.c中的AddTextPrinter把延迟减一后存为打印器的速度;打印器在显示一个字符时把计数器设为这个存储值,每帧减一,并在计数器归零后的下一帧显示下一个字符。所以一个字符花的就是延迟本身:8、4和1帧。在60赫兹下即每秒7.5、15和60个字符。1213 这种计法是我对打印器状态机(src/text.c中的AddTextPrinter和RENDER_STATE_HANDLE_CHAR分支)的解读,脚本对它做的是断言而不是运行。1 Game Boy Advance并不精确地以60赫兹运行:GBATEK给出的帧是“Total 228 lines, 16.743 ms, 280896 cycles - ca. 59.737 Hz”(共228行,16.743毫秒,280896个周期,约59.737赫兹),其中160行可见,每行宽240点,所以在实机上最快设置约为每秒59.7个字符。3233

按住按钮会让较慢的设置变快。在某个字符的延迟倒数期间重新按下A或B,在允许这样做的消息中(gTextFlags.canABSpeedUpPrint),会设置hasPrintBeenSpedUp。此后打印器检查JOY_HELD(A_BUTTON | B_BUTTON) && subStruct->hasPrintBeenSpedUp,成立时把计数器降为0:每帧一个字符,每秒60个,与最快设置的速度相同。131

自动滚动的文字有自己的延迟。当gTextFlags.autoScroll开启时,打印器把计数器设为3而不是存储的速度,即每个字符4帧,但录像对战除外:判断条件是if (!(gBattleTypeFlags & BATTLE_TYPE_RECORDED) && gTextFlags.autoScroll)。131 战斗信息在联机对战和录像对战中开启自动滚动,messageautoscroll脚本命令也会开启自动滚动并强制使用中速。34 因此,延迟3可能适用于联机对战中的其他战斗窗口和脚本触发的自动滚动消息,而在录像对战中永远不适用;录像对战的消息以打印器得到的速度显示:对战斗信息而言,就是随录像保存的文字速度,唯有录像的联机对战例外,其消息窗口像联机对战一样被赋予1。341 依我的解读,计数器只在打印器存储的速度大于0时运转,而联机对战的消息窗口被赋予延迟1,这使得即便开启自动滚动,其文字也仍是每帧一个字符。1334

三个换行代码各司其职。\n把打印器移到第二行;\l在翻页箭头下等待按键,然后把对话框向上滚动一行;\p同样等待,然后清空对话框,从顶部重新开始。1335 滚动时,16像素的一行按速度设置每帧上移1、2或4像素(sWindowVerticalScrollSpeeds),因此一次滚动需要16、8或4帧。113

60赫兹下每秒字符数的横向条形图:《绿宝石》慢7.5、中15、快60,按住A或B时60;《水晶》和《红》慢12、中20、快60;《最终幻想VI》场景60;以及Kiradex简报提议的Slow 12和Normal 30。

测量的每款游戏的文字速度,以及简报提议的两个设置;由figures/make_figures.py根据测量输出绘制。

箭头

一页写满后,对话框在一个小箭头下等待。《绿宝石》以{ 0, 1, 2, 1 }像素的纵向偏移(sDownArrowYCoords)绘制一个8×16的箭头单元格,每当重置为8的延迟耗尽就重绘一次。也就是每9帧一步,一个周期36帧,在60赫兹下为0.60秒。113 这个箭头不闪烁;它在三个位置间来回上下浮动。

对话框与它的边框

《绿宝石》场景中的文本框由src/menu.c中的sStandardTextBox_WindowTemplates定义。文本窗口位于图块第2列、第15行,大小为27×4图块,即216×32像素,容纳两行16像素的常规字体。它的边框由WindowFunc_DrawDialogueFrame绘制,脚本针对该窗口求值后,范围是第0到29列、第14到19行:30×6图块,240×48像素,占满屏幕宽度,是160像素高度的30%。12

这个边框不是九宫格。LoadMessageBoxGfx从message_box.png加载它,这是一条56×16像素、14个图块的长条,边框由八个图块位置绘制,底边那一行是用BG_TILE_V_FLIP把顶边那一行上下翻转得到的。1362 脚本测量的是边框的范围和PNG的尺寸;翻转是从填充调用中读出来的。1

玩家可选的边框是给其他窗口用的:菜单和“是/否”框。WINDOW_FRAMES_COUNT是20,这二十种边框都是24×24像素的PNG,即3×3个8像素图块,由LoadUserWindowBorderGfx根据选项画面FRAME一项保存的边框类型加载。136

这就纠正了世界篇。那篇的规则10写道:“Pokémon Emerald’s text-window frame is a 24-by-24 image, three by three hardware tiles, which is a nine-slice”(《宝可梦 绿宝石》的文本窗口边框是一张24×24的图,3×3个硬件图块,即九宫格)。23 graphics/text_window/中那二十张24×24的图,是菜单窗口可选的边框。对话框,也就是玩家记忆中的那个文本框,是从56×16像素的message_box.png绘制成底边镜像的30×6图块边框,而不是一张24×24图片的九宫格。136 九宫格这个思路本身成立,菜单也在用它;错的是把它归到对话框上。

以相同比例绘制成8×8像素图块网格的两个屏幕:《绿宝石》的30×20图块,对话边框覆盖底部6行,其内是从第2列第15行开始的27×4文本窗口;《水晶》的20×18图块,底部为20×6的对话框,其内为18×4。

把两个对话框画成图块网格,不含任何游戏美术;由figures/make_figures.py绘制,并将各个范围与measure_gen_text.out逐一核对。

文字是为对话框而写的

为了看对话框填得有多满,脚本读遍了《绿宝石》剧本文字的每一行:504个文件,即data/text/*.inc的36个和468张地图的scripts.inc,不含占位符的行24,100行,含占位符的3,000行,按游戏的三个换行代码\n、\l和\p切分。中位数的一行为29个字符、150像素;第95百分位为38个字符、196像素;最长的一行53个字符,最宽的一行208像素。没有一行宽于216像素的窗口,平均字符宽5.25像素。137

按每个字符5.25像素,窗口能容纳41个;编剧们让95%的行停在38个。331 每一处换行都是手工放置的代码。游戏从不自动折行,它信任编剧。

对话框只有一个窗口,没有名字栏。编剧想标出说话人时,就把名字和冒号打进行里,比如未白镇的“MOM: {PLAYER}, we’re here, honey!”。14,412个段落中有934个,即6.5%,以首字母大写的名字和冒号开头。137 没有单独的名字栏,是我对窗口模板的解读:模板只为两行文字留了位置,别无他物。2

英文版里的日语

英文版《绿宝石》仍带着日语字形,用于来自日版卡带的宝可梦和训练家的名字。打印器在控制码EXT_CTRL_CODE_JPN处切换字形集,在EXT_CTRL_CODE_ENG处切换回来,这类名字都带有这些控制码(由international_string_util.c、contest.c和battle_tower.c插入)。在这种模式下,常规字体的每个字符都宽8像素、高15像素(DecompressGlyph_Normal,而GetGlyphWidth_Normal返回8)。11338

英文版ROM构建时使用的字表如下:japanese_normal.png为128×512像素,以8×16为单元格,512个单元格中画了302个;japanese_small.png同样大小,画了307个;japanese_bold.png为128×256,256个中画了246个;还有《火红》的japanese_frlg_male.png,它也在《绿宝石》的代码树里,为256×512像素,以16×16为单元格,512个中画了269个,并带有一张512项、取值0到10像素的宽度表,其中408项为10。138 pret构建的是英文版游戏,所以这些是英文版ROM中用于名字的字表;日版ROM本身的对话框、速度和字体都没有读(第11节)。30

所以按经典作品自身的尺度,一个日语字符在16像素的一行里,在《绿宝石》中宽8像素,在《火红》的字表中大多为10像素,而拉丁字母为4到6像素。1

《火红》

《火红》沿用了《绿宝石》的延迟8、4和1,并像《绿宝石》一样由src/text_printer.c中它自己的AddTextPrinter减一后存储;它的标准文本窗口是第2列的26×4图块(src/new_menu_helpers.c),比《绿宝石》窄一个图块。139 它的打印器也有《绿宝石》的按住处理:if (JOY_HELD(A_BUTTON | B_BUTTON) && subStruct->hasPrintBeenSpedUp)把计数器降为0,而在某个字符的延迟倒数期间重新按下A或B,在允许的消息中会设置hasPrintBeenSpedUp。14

2. 《水晶》与《红》:十八个字符和一个闪烁的标记

Game Boy的对话框是Game Boy Advance对话框在更低分辨率下的祖先。《水晶》的屏幕为20×18图块(SCREEN_WIDTH 20、SCREEN_HEIGHT 18),即160×144像素。它的文本框是TEXTBOX_WIDTH = SCREEN_WIDTH乘TEXTBOX_HEIGHT 6:占满宽度,高度的三分之一,即33%。由于BORDER_WIDTH 2,内部为18×4图块,8×8的字体每行18个字符。文字从框内第二行开始,TEXTBOX_INNERY = TEXTBOX_Y + 2,行与行相隔两行,所以框中显示两行,中间隔一个空行;两行布局是我对这些常量的解读。13 《红》的消息框是db MESSAGE_BOX, 0, 12, 19, 17:位于底部的同样20×6图块。112

这两款游戏也以帧计速度。延迟为TEXT_DELAY_FAST 1、中速3、慢速5帧一个字母:在60赫兹下每秒60、20和12个字母。《水晶》的PrintLetterDelay说得很直白:“The text speed setting in wOptions is actually a frame count”(wOptions中的文字速度设置其实是一个帧数)。按住A或B时,每个字母等待一帧。115 所以《水晶》的最快设置与《绿宝石》的最快设置相同,都是每秒60个,在《绿宝石》中按住A或B也是这个速度;《水晶》的中速和慢速,20和12,比《绿宝石》的15和7.5更快。1 Pan Docs给出的Game Boy帧是“One frame: 70224 dots @ 59.7 fps”(一帧:70224点,59.7fps),并注明一帧“takes ~16.74 ms instead of ~16.67”(约需16.74毫秒,而非约16.67毫秒)。40

《水晶》让玩家从八种边框FRAME_1到FRAME_8中选择,每种都是TEXTBOX_FRAME_TILES,即6个1位图块,存为24×16像素的PNG;ROM里还有一个未使用的第九种边框。字体字表gfx/font/font.png为128×64像素,128个8×8单元格,《红》的也是同样大小。141

标记会闪烁

《水晶》的翻页标记是边框的一部分。WaitPressAorB_BlinkCursor等待A或B时,BlinkCursor把位于边框自身最底行(18, 17)处的图块在'▼'与边框图块'─'之间来回切换。16 这两种状态是从代码中读出来的;闪烁的周期没有推导(第11节)。

这是世界篇的第二处不精确。它的文字计划要求“a two-frame advance arrow”(一个两帧的翻页箭头)。23 两种状态的标记是《水晶》的闪烁。而那份计划其余部分(16像素的行、底部的对话框)所遵循的《绿宝石》箭头并不闪烁:它从静止位置以0、1、2、1像素经过三个位置,周期36帧(代码偏移的是位图的源行,依我的解读这会让楔形在屏幕上抬起;我没有运行过)。113 第12节的简报沿用《绿宝石》的步进,但不沿用它的方向。

3. 《最终幻想VI》:可以重新上色的窗口,会自动折行的字体

《最终幻想VI》把窗口当作玩家的东西。这里的数字来自everything8215/ff6反汇编的8130132(2026年9月14日),它收录的是代码和提取列表而不是ROM的图像,所以尺寸取自字节范围以及读取它们的代码。304

八种窗口,每种七色

英文版和日文版的提取列表都把窗口图像标为0xED0000到0xED1BFF,共7,168字节:八种样式,每种896字节,即28个每像素4位的8×8图块。其调色板紧随其后,位于0xED1C00到0xED1CFF,八个16色调色板。442 设置画面列出8种窗口样式(ConfigWindowNumPos)。443

窗口的颜色由玩家决定。LoadWindowPal从存档内存$1d57处按每种样式14字节读取颜色,也就是七种两字节的颜色,设置画面则用R、G、B三条滑杆编辑它们(ConfigColorBarTextList);另有一种颜色用于字体,位于$1d55。443 步长是从代码中读出的,三个通道则来自滑杆列表。4

设置画面的信息速度有六档,1到6(ChangeConfigOption_02在$05处停止),新存档以“message speed = 3, battle speed = 3”(信息速度3,战斗速度3)开始,这是menu_sram.asm中注释的说法。443 这是战斗信息的速度,config.asm的一条注释也这样称呼它:“battle speed, battle message speed, magic order, and window pattern”(战斗速度、战斗信息速度、魔法顺序和窗口样式)。在设置画面之外,唯一读取设置字节$1d4d中这几位的代码,是src/btlgfx/gfx_cmd.asm中的mess_wait,它在MsgSpeedTbl,即“wait duration for each battle message speed”(每档战斗信息速度的等待时长)中查出设置,然后等待相应帧数:设置1到6分别为32、48、64、80、96或112帧。场景代码中没有任何文件读取$1d4d,所以场景对话没有速度设置;它以下文所述的唯一速度显示。444

不用图块的渐变窗口

对话窗口由硬件的色彩运算自上而下加上渐变。src/field/hdma.asm中的DlgWindowFixedColorTbl是200字节的$E0到$E7,在三个通道上都给出强度0到7的固定色,第一段是72个值,从7开始,逐步降到0,再升回7。445 窗口在屏幕顶部或底部附近打开,event.asm注释中的说法是“dialog window y position (1 or 18)”(对话窗口的y位置,1或18)。45 这张表每条扫描线消耗一个值,是从注释中读到的,没有追踪;渐变看起来是什么样子也没看到,因为反汇编里没有像素(第11节)。

字体

对话字体每像素1位,每个字母22字节:11行16位,单元格宽16像素、高11像素。它的投影并不是画出来的:LoadLetterGfx把每一行移位一个像素(lsr)来生成它。446 由FontWidth给出的英文宽度是比例的:大写字母平均8.04像素(5到12),小写7.73(4到12),数字7.80(5到9),空格为5。446

日文版ROM的对话字体在其提取列表中位于0xC494C0到0xC4FFFF,共27,456字节:1,248个22字节的字母。它的宽度表有1,280项,依次为1,056个12像素、180个8像素和44个12像素,反汇编的字符表列出了1,010个汉字和173个假名。447 英文版大字体的范围为5,120字节,不是22字节字母的整数个(232.7),这一点我无法从反汇编中解释(第11节)。4

一行、一次折行、每帧一个字母

一行对话从x 4延伸到最大x $e0,即224:220像素,12像素的日语可放18个字母。446 英文文字由代码按单词折行。CalcTextWidth加上下一个单词的宽度,一旦会超出最大值就调用NewLine,所以英文剧本不需要手工换行;脚本对这些调用做了断言,解读的对象是UpdateDlgTextOneLine。446

文字每帧显示一个字母。场景循环等待垂直消隐,每轮调用一次UpdateDlgText,而UpdateDlgTextOneLine画一个字母就返回:在60赫兹下每秒60个字母。448 每帧一轮是我对这个循环的解读。由于同一份源码构建两个ROM,两种语言共用这个循环,依我的解读,这意味着英文和日文每秒显示的字母数相同。48

地图标题居中显示,($e0 - width) / 2,持续100帧,即1.67秒。446

与《绿宝石》对照,《最终幻想VI》展示了做同一个对话框的另一种方法:《绿宝石》手写每一处换行,只让玩家选择菜单的边框而从不让选对话框的;《最终幻想VI》由代码折行,让玩家重新给窗口上色,并用硬件加上渐变。《绿宝石》的场景对话框在底部,《最终幻想VI》的在顶部或底部,两者都以固定的帧数显示。

《时空之轮》

我原本打算把《时空之轮》(Chrono Trigger)与《最终幻想VI》并列,但没能做到。我没有克隆任何公开的反汇编,而Data Crystal上该游戏的ROM地图在获取时返回HTTP 403,所以本文关于文本框的内容没有一处来自它(第11节)。49

4. 8、10和12点阵的日语

Kiradex是一款面向卡牌收藏者的应用,图鉴已经用系统字体显示日文卡名,并在下方附上英文。50 世界里的文字也将需要日语,不能显示日语的像素字体就不在候选之列。问题在于汉字能做到多小,以及谁已经画好了七千个。

工作量

JIS X 0208是衡量日语像素字体覆盖范围的标准字符集,按Unicode联盟的映射表共有6,879个字符:524个非汉字,其中169个是第4和第5区的假名;第16到47区的2,965个第一水准汉字;以及第48到84区的3,390个第二水准汉字。576 这张表是2.0版,联盟把它归在已废弃的映射之下;但它仍是统计字体覆盖范围最方便的清单。5

Misaki:8×8

Num Kadoma制作的Misaki(美咲フォント)是最小的一款。它的页面称其为“8×8 ドットの日本語ビットマップフォント”(8×8点阵的日语位图字体),支持“JIS 第一・第二水準”(JIS第一、第二水准),并说“一部の記号を除いた全ての文字は 7×7 ドットの範囲に収まっている”(除部分符号外,所有字符都收在7×7点阵的范围内),页面接着说,这样字符并排时仍能保留行距和字距。51 据页面的沿革,它是为PC-E500袖珍电脑的日语显示驱动而做的。51

从2021-05-05版的BDF文件测量,Misaki Gothic有7,171个字符:168个半角,宽4像素;7,003个全角,宽8像素;单元格8×8,上伸部为6,下伸部为2。在U+4E00到U+9FFF的6,356个字形中(这是码位范围,所以与JIS两级水准合计的6,355个汉字数目不同),没有一个在任一方向上填满8点的单元格,而有53个全角字形,都是符号,填满了。它画齐了JIS X 0208的全部6,879个字符。6 Misaki Mincho按其页面的描述是把Misaki Gothic的非汉字重绘为明朝体风格,各项计数完全相同。Misaki Gothic 2nd按其页面的说法,半角字符用满3×7像素,全角假名用满7×7,它有7,170个字符(167个半角),上伸部为7,下伸部为1,其余各项相同。651

k8x12:8×12

同一作者的k8x12保留8点的宽度而增加高度:“8×12 ドットの日本語ビットマップフォント”(8×12点阵的日语位图字体),JIS第一、第二水准。它有两个变体:假名画得较高的k8x12L,以及非汉字为8点高的k8x12S,页面说后者是考虑到旧游戏机的限制而做的:“8×8 ドット単位でグラフィックを扱うような、昔のゲーム機等の制限を意識しています”(意识到了以8×8点为单位处理图形的旧游戏机等的限制)。52

从BDF测量,k8x12的单元格为8×12,上伸部10,下伸部2:7,170个字符,168个为4像素,7,002个为8像素。没有汉字填满单元格;填满单元格的全角字形在k8x12和k8x12L中有53个,在k8x12S中有45个。三者都画齐了JIS X 0208的全部6,879个字符。6

PixelMplus:10与12,以轮廓的形式

itouhiro制作的PixelMplus(2013年)走的是另一条路:看起来像位图字体的TrueType轮廓,不含嵌入位图(“埋め込みビットマップは なし”),收录JIS第一、第二水准的全部汉字(“JIS第1・第2水準のすべての漢字を収録”)以及ISO-8859-1,有常规和粗体两种字重、PixelMplus12和PixelMplus10两种尺寸,由M+ BITMAP FONTS轮廓化而来。18 用Core Text测量,每款都有7,253个字形,画出了JIS X 0208 6,879个字符中的6,876个。没有映射的三个是U+301C 〜、U+2016 ‖和U+2212 −。7 这几个字符在联盟的JIS0208.TXT与其他厂商的表之间Unicode映射不同,所以依我的解读,这些字体很可能把字形放在了其他码位上;我没有核实(第11节)。5

DotGothic16:不在网格上的点阵字体

Fontworks的DotGothic16在Google Fonts上以SIL Open Font License 1.1发布,没有保留字体名,列出的子集为cyrillic、japanese、latin和latin-ext,于2020-12-15加入。53 它有9,362个字形,画齐了JIS X 0208的全部6,879个字符。7 但它画的是分离的点,而不是像素。在它的A中,轮廓的x边界成对出现,以1,000单位计为91和102、153和163、214和225、276和287、338和349、400和410:点与点相距约62单位,中间有10或11单位的空隙。没有任何整数像素网格能对上它,探针在每em 4到40像素之间一个网格也没找到。197 约62单位的点距接近每em 16点,正是字体名中的数字,所以探针按名义上的每em 16点及其倍数来绘制它;这是我选的尺寸,不是字体声明的网格。macOS 27.0上的Core Text把它在16像素下绘制为82.3%中间调,64像素下为37.3%,测量的每个尺寸都有中间调(第6节)。7

免费字体的测量结果

表中的设计网格,是使样本字形95%的轮廓点落在0.04像素以内的最小每em像素数,每个轴允许一个固定偏移;求它的脚本measure_coretext.swift用swiftc -O编译,在macOS 27.0(版本26A428)上运行,所以这些是Mac上Core Text的数字,不是iPhone的。7

字体 字形数 位图表 设计网格(每em像素) 拉丁字母/假名步进(像素) 画出的JIS X 0208
Misaki Gothic 7,174 EBDT、EBLC 8,在基线上 4 / 8 6,879
k8x12 7,173 EBDT、EBLC 12,在基线上 4 / 8.01 6,879
PixelMplus10 Regular和Bold 各7,253 无 10,在基线上 5 / 10 6,876
PixelMplus12 7,253 无 12,在基线下方0.39像素 6 / 12 6,876
DotGothic16 9,362 无 无(按名义16点绘制) 8 / 16名义点 6,879

按这一标准,PixelMplus10 Regular的1,032个样本轮廓坐标中没有一个偏离其10像素网格,Bold的1,000个中也没有;Misaki Gothic的848个中为0,k8x12的988个中为0;PixelMplus12在允许0.39像素偏移后,1,192个中为0;而DotGothic16的1,544个全部偏离。7 PixelMplus12的偏移在原始坐标里就能看出:它的H在以1,000为单位时从y −33延伸到717,网格为每像素83.3单位,所以它的基线落在网格行内0.396像素处;探针的估计值0.39,是把坐标的小数部分分到宽度为五十分之一像素的区间里得出的,下文的渲染就按这个数值偏移。19733

在测量的每款支持日语的字体中,一个日语字符都是拉丁字母的两倍:Misaki和k8x12中为8对4,PixelMplus中为10对5和12对6。7 经典作品也在同一范围内:《绿宝石》的日语在16像素的一行里宽8像素,《火红》的字表大多为10,《最终幻想VI》为12。14

授权

这一切都以最直白的条款免费提供。作者字体页面上Misaki和k8x12的授权只有三句话:“These fonts are free software. Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially. THESE FONTS ARE PROVIDED “AS IS” WITHOUT WARRANTY.”(这些字体是自由软件。无论是否修改、无论商业或非商业,均无限制地允许使用、复制和分发。这些字体按“原样”提供,不附任何担保。)每个压缩包的readme以略有不同的措辞载明同样的条款,Misaki的页面也写明其所用授权为M+ FONTS授权。1751 PixelMplus采用M+ FONT LICENSE,其英文文本LICENSE_E就是同样的三句话。18 DotGothic16采用SIL Open Font License 1.1,条款见其自带的OFL.txt。53 依我的解读,三句话的授权不要求任何回报;但注明作者仍是应当的,简报也这样做了(第12节)。这些是我的解读,不是法律意见。

可读性的代价

Misaki中的一个假名宽8个字体像素,PixelMplus12中为12。在iPhone 18 Pro Max上以世界本身的密度,也就是一个纹素等于8个设备像素时,用Misaki绘制的文本框一行能容纳36个拉丁字母或18个日语字符。11 日语的一行每个字符承载的信息更多,所以文本框不需要容纳同样多的字符也能表达同样的意思;这是我的解读,不是测量。

5. 拉丁字母像素字体,以及我没能测量的那些

与日语字体一起,我还测量了Google Fonts上的两款拉丁字母像素字体。30

Press Start 2P由CodeMan38制作,采用SIL Open Font License 1.1,“with Reserved Font Name “Press Start 2P””(附保留字体名”Press Start 2P”)。54 它有658个字形,位于基线上的8像素网格,每个拉丁字母宽8像素,包含全部95个可打印ASCII字符。它没有假名或汉字:在JIS X 0208中,它画了150个非汉字(希腊字母、西里尔字母和符号),6,355个汉字一个都没有。7

Silkscreen由Jason Kottke制作,采用SIL Open Font License 1.1,没有保留字体名。55 它有228个字形,位于8像素网格,588个样本轮廓坐标中有10个偏离网格(它的A和o在125单位的网格上有y值495和130),它的A宽6像素。在JIS X 0208中,它画了21个非汉字。719

还有三款常被推荐给像素游戏的字体没有测量,我如实说明,而不是凭记忆去描述它们。

Pixel Operator,作者Jayvee Enaguas:作者在DaFont上的说明写道“Changed licence to Creative Commons Zero (CC0) 1.0”(授权已改为CC0 1.0),并说这款字体“licensed under a Creative Commons Zero (CC0) 1.0”(以CC0 1.0授权)。DaFont把它列为“Bitmap (16 px)”,共15个字体文件,241个字形。说明中给出的作者在NotABug上的仓库返回HTTP 404,字体既没有下载也没有测量。这里的一切都来自那一个页面。56

m5x7,作者Daniel Linssen:它的itch.io页面写着“use font size 16, 32, 48, etc”(请使用字号16、32、48等)和“free to use but attribution appreciated”(可免费使用,但欢迎署名),列出“Asset license Creative Commons Zero v1.0 Universal”,并给出平均宽度5、上伸部7、下伸部2、x高度5。它对自己字符的描述是“uppercase and lowercase letters, including many accented letters, numbers and most common symbols”(大小写字母,包括许多带变音符号的字母、数字以及大多数常用符号)。57

m3x6,同一作者:“use font size 16, 32, 48, etc”和“free to use with attribution”(署名即可免费使用);它的页面没有素材授权一栏。58 这两款的文件都在“自定价格”的下载之后,所以都没有下载,也都没有测量。5758

这是世界篇的第三处不精确。它的文字计划指定m5x7为字体。23 按其页面自己的描述,m5x7是一款拉丁字母字体,页面上没有任何地方说它有假名或汉字;一个将要显示日语的应用,不能用它来排世界里的文字。57 简报把字体改为PixelMplus10,它是测量过的、同时具备日语、干净网格和整数像素步进的两款字体之一(另一款是Misaki),也是两者中较大的一款(第6节和第12节)。m5x7本身仍未经测量。

6. Core Text对像素字体做了什么

世界篇断言“Bitmap fonts render at integer multiples only”(位图字体只能以整数倍渲染)。23 这是每一份像素美术指南都在重复的规则,而我想在Apple自己的文字引擎上,用数字亲眼看一看。

测量方法

Core Text是Apple用于“Create text layouts, optimize font handling, and access font metrics and glyph data”(创建文字布局、优化字体处理、访问字体度量和字形数据)的一层,自iOS 3.2和macOS 10.8起可用。59 measure_coretext.swift逐个加载候选的TrueType文件,把一行样本文字(拉丁字母字体用“Nice card! Which set? 0123”,日语字体用“カードを見せて! Nice card!”)以各字体网格的整数倍和非整数倍尺寸绘制到8位灰度位图中,然后统计着墨像素中属于中间调的那些:255中的8到247,与全黑或全白相差超过3%。每个尺寸绘制三遍:Core Text默认的抗锯齿,关闭抗锯齿,以及开启子像素定位并关闭量化,让字形精确落在其步进所指的位置。7

在看任何数字之前,有两个限制很重要。运行环境是我Mac上的macOS 27.0,版本26A428,不是iOS;绘制是通过Core Text画进位图上下文,而不是通过SwiftUI的Text;iPhone会画出什么,在简报的截图检查在iPhone上运行之前,都只是预测(第11节)。7 而且脚本是在关闭字体平滑(setAllowsFontSmoothing(false))的情况下绘制的。我改用位图上下文的默认设置绘制下图时,PixelMplus10在10像素下的“Nice card!”,272个着墨像素中有178个是中间调,而关闭平滑时94个中为0。9 字体平滑是macOS的设置;iOS上是否有类似的东西起作用,要由截图检查来决定。

整数倍是清晰的

在网格的整数倍下,开启抗锯齿(上下文的默认)并关闭字体平滑时,Misaki在8、16、24和32像素下,PixelMplus10 Regular和Bold在10、20、30和40像素下,Press Start 2P在8、16、24和32像素下,渲染出的中间调像素都是0.0%。7 每个像素非墨即纸。这取决于平滑设置:在位图上下文的默认平滑下,PixelMplus10以其本身的10像素绘制时,“Nice card!”的272个着墨像素中有178个是中间调,所以在Mac上,仅有整数倍并不能让文字清晰。9

PixelMplus10 Regular以10像素、关闭抗锯齿绘制,并在网格上放大九倍后的像素:50×11字体像素的Nice card!一行,以及80×11的日语一行カードを見せて!,基线位于从底部往上两行处。

macOS 27.0上的Core Text以每字体像素一个设备像素绘制的PixelMplus10 Regular,以关闭抗锯齿的状态展示。开启抗锯齿并关闭字体平滑时,“Nice card!”的94个着墨像素中0个是中间调;在位图上下文的默认平滑下,272个中有178个。该字体采用M+ FONT LICENSE,允许复制和分发;由figures/dump_glyphs.swift和figures/make_figures.py绘制。

整数倍之间会发虚

在整数倍之间,同样这些字体会发虚,平滑与整个探针一样保持关闭。Misaki在12像素(网格的1.5倍)下为57.7%中间调,在20像素(2.5倍)下为39.9%,在10像素(1.25倍)下为82.2%。PixelMplus10在15像素下为60.1%。Press Start 2P在12像素下为44.0%,20像素下29.1%,28像素下21.7%。7 对于网格位于基线上的八款字体(两款Misaki、k8x12和k8x12S、PixelMplus10 Regular和Bold、Press Start 2P以及Silkscreen),以网格的1.25、1.5、1.75、2.5和3.5倍进行的全部40次渲染中,中间调比例从19.4%(PixelMplus10 Bold的35像素)到82.2%(Misaki的10像素)。7 发虚并不随尺寸稳定下降:在这八款中的每一款里,网格的1.75倍都比1.5倍更虚(Misaki在14像素下为65.7%,12像素下为57.7%)。在相同的小数部分下则会下降:在网格的1.5、2.5和3.5倍处,八款字体每一款都逐级降低(Misaki为57.7%、39.9%和30.4%)。在整数倍之间测得的每一次渲染中,它都没有降到零;依我的解读,固定小数部分时会下降,是因为字形边缘半个像素的误差,在越大的字形中所占的比例越小。7

关闭抗锯齿(setShouldAntialias(false))后,每个尺寸下每个像素都非全即空,但形状被不均匀地重新采样。Misaki在8像素下的191个着墨像素,到10像素下变成377个;若在每个方向上均匀放大1.25倍,应约为298个。733 有些行和列被加倍,有些没有,原本宽一像素的笔画会变成一像素或两像素。

关闭字体平滑时,中间调像素比例相对于各字体网格倍数(1到4)的折线图:Misaki、PixelMplus10 Regular和Press Start 2P在每个整数倍处降到0%,在其间升到21.7%(Press Start 2P的3.5倍)至82.2%(Misaki的1.25倍)之间;PixelMplus12从未降到0%,从1倍的66.6%到4倍的23.6%;字形被精确放置的k8x12也从未降到0%,从1倍的62.3%到4倍的25.9%。

按尺寸统计的中间调像素,macOS 27.0上的Core Text,关闭字体平滑,不是iOS;由figures/make_figures.py根据measure_coretext.out绘制。

清晰的尺寸还不够:文件本身必须干净

对选择字体而言,两种更隐蔽的失败比倍数更重要。

偏离基线的网格在每个尺寸下都会发虚。 PixelMplus12的轮廓位于基线网格下方0.39像素,在其本身的12像素下渲染为66.6%中间调,24像素下40.0%,36像素下29.8%。在开启子像素定位的情况下,通过上下文变换把基线移动这个偏移量,可以让12像素降到0.0%,但24像素仍剩14.3%,36像素仍剩19.0%,尽管偏移量是按尺寸同比放大的(24像素下0.78像素,36像素下1.17像素)。7 为什么放大后的偏移清理不了更大的尺寸,输出没有说明,我也没有追查。

偏离网格的步进在字形被精确放置后会发虚。 k8x12的假名步进是8.01像素,而不是8。在把每个字形放在整数像素上的位图上下文默认设置下,它在12、24和36像素下渲染为0.0%中间调。开启子像素定位并关闭量化后,同样这些尺寸分别为62.3%、42.8%和32.6%中间调。7 依我的解读,这时每个假名都比前一个离网格再远百分之一像素。

轮廓上的离群点会留下一点痕迹。 Silkscreen那十个偏离网格的坐标,在8像素下表现为4.5%中间调,在32像素下为1.2%。7

点阵字体永远对不齐。 DotGothic16没有可以取倍数的网格,所以探针按名义上的每em 16点及其1.25到4倍来绘制它(第4节)。开启抗锯齿并关闭平滑时,它在16像素下为82.3%中间调,32像素下52.5%,48像素下48.8%,64像素下37.3%,在16到64像素的全部九个尺寸下都有中间调,包括16的整数倍;它的点不落在任何像素网格上(第4节)。719

Misaki和k8x12带有嵌入位图表(EBDT和EBLC,用CTFontCopyAvailableTables读出)。上面的测量结果是Core Text画出来的东西;它画的是那些位图还是轮廓,没有确定(第11节)。7

Misaki、PixelMplus10和Press Start 2P没有这两种隐蔽的缺陷。由此得出的规则既关乎尺寸,也关乎字体文件:整数倍,位于基线上的网格,以及整数像素的步进。

Dynamic Type与网格

Apple的Human Interface Guidelines给出iOS的默认文字尺寸为17点、最小为11点,并为每一个Dynamic Type尺寸列出了表格。Body从xSmall到AX5依次为14、15、16、17、19、21、23、28、33、40、47和53点;Caption 1从11到43。6010

在3×屏幕上,只有当点数乘以3再除以以字体像素计的em得到整数时,一个字体像素才等于整数个设备像素。在十二档Body尺寸中,12像素的em有3档为整数(16、28和40点),8像素的em有2档(16和40),10像素的em只有1档(40)。在Caption 1的十二档尺寸中,12像素的em有3档为整数(12、16和32)。760

iOS十二档Body尺寸(xSmall到AX5,14到53点)的网格:实心点表示在3×屏幕上8像素em为整数之处(16和40点)、10像素em(仅40)和12像素em(16、28和40);下方是简报为PixelMplus10取整后的倍数m(半数进一),4、5、5、5、6、6、7、8、10、12、14和16,以及由此得到的点数13.33到53.33。

哪些Body尺寸在3×屏幕上落在整数设备像素上,以及简报的取整;由figures/make_figures.py根据measure_coretext.out和measure_kiradex_text.out绘制。

所以像素字体不能简单地跟随Dynamic Type。如果它随系统尺寸连续缩放,那么除了少数几个整数尺寸之外,它在每个尺寸下都偏离网格:10像素的em有12档Body尺寸中的11档,12像素的em有12档中的9档。733 一旦偏离网格,开启抗锯齿的Mac版Core Text会让测量的每一款字体发虚;iPhone上的SwiftUI是否也如此,取决于它的渲染和放置方式,要由简报的截图来决定。它必须接受系统要求的尺寸并将其取整到网格上,第10节把这一点变成API配方,第12节把它变成简报的规则。

7. 日语换行

一句简短的预设台词从不折行。文本框里的一段文字会折行,而日语对在哪里折行有规则。

规则

W3C的《日语排版需求》(Requirements for Japanese Text Layout,2020年8月11日的工作组说明)在3.1.7中规定了行首规则:“In principle, no line should begin with closing brackets (cl-02), hyphens (cl-03), dividing punctuation marks (cl-04), middle dots (cl-05), full stops (cl-06), commas (cl-07), iteration marks (cl-09), a prolonged sound mark (cl-10), small kana (cl-11) or warichu closing brackets (cl-29)”(原则上,任何一行都不应以后括号、连字符、分隔标点、中点、句号、逗号、叠字符、长音符、小写假名或割注后括号开头)。它也记录了较宽松的做法:“Not a small number of books adopt a less strict set of rules which allow IDEOGRAPHIC ITERATION MARK “々” (one of the iteration marks (cl-09)), prolonged sound mark (cl-10) and small kana (cl-11) to start a line.”(不少书籍采用较宽松的规则,允许々、长音符和小写假名位于行首。)21

Unicode的换行算法UAX #14(第57版,对应Unicode 18.0.0,日期2026-09-01)把小写平假名、片假名和长音符归入CJ类,并说:“Treating characters of class CJ as class NS will give CSS strict line breaking; treating them as class ID will give CSS normal breaking.”(把CJ类字符当作NS类处理,得到CSS的strict换行;当作ID类处理,得到CSS的normal换行。)61

Core Text的表现

measure_coretext.swift在macOS 27.0(不是iOS)上运行,用CTFramesetter把一段131个字符的日语段落排在8、10、12、14、16、18和20 em的宽度下,并把第一行之后每一行的首字符与它自己的行首禁则字符列表对照:部分后括号、日语逗号和句号、中点、冒号和分号、问号和感叹号、长音符、小写假名以及々。7 这份列表不是JLREQ规则所列各类的子集,只是与它们有交集。它的40个字符中有7个,即,.:;?!和),是日语文本在JLREQ列出ASCII字符之处所用的全角形式;其余33个是JLREQ自己的字符。在后括号中,它收了」』】〉》〕和全角的),没有收’ ” ] } » 〟 ⦆ 〙 〗和ASCII的);在40个小写假名中,它收了22个,没有收ゕ、ゖ以及ㇰ到ㇿ;它也没有收连字符类(cl-03),包括‐、〜、゠和en dash,没有收cl-04的‼ ⁇ ⁈ ⁉,也没有收除々之外的任何叠字符(cl-09),包括ヽ、ヾ、ゝ、ゞ和〻。721 下面的结果针对的是探针的列表,而哪些字符会出现在行首,由测试段落的文字决定。

不加标签时,无论在Hiragino Sans(系统的日语字体,作为对照运行)还是八款像素字体中的任何一款,无论哪个宽度,都没有一行以探针列表中的字符开头。这八款是Misaki Gothic、Misaki Gothic 2nd、k8x12、k8x12S、PixelMplus10 Regular和Bold、PixelMplus12,以及按名义16点排版的DotGothic16。7

用kCTLanguageAttributeName设为ja加上标签后,规则放宽了。在Hiragino Sans、两款Misaki、两款PixelMplus10、PixelMplus12和DotGothic16中,7个宽度中有6个,即除18 em外的每个宽度,都有一行以小写假名(っ、ョ或ァ)开头。7 在k8x12和k8x12S中,7个宽度中有4个有一行以小写假名(っ、ョ或ょ)开头,另有两个宽度出现以长音符ー开头的行,即10 em(与ョ同一宽度)和18 em,后者是第五个宽度。7

把这些计数与两段引文对照,依我的解读,ja标签给出的是JLREQ所描述的较宽松规则,也就是UAX #14所说的CSS normal换行,而不加标签的运行表现为严格规则。2161 这与我的预期相反,因为标签指明的是语言,这也是简报在没有截图证据前不给文本框文字加标签的原因。

关于测量本身有两点说明,都来自对照输出阅读脚本。宽度以em计,即字体的完整尺寸,所以对于假名在12像素em中宽8像素的k8x12,每个宽度容纳的假名是数字所示的1.5倍;k8x12的段落在8 em下排成12行,而其他字体为17行。7 另外,没有网格的DotGothic16是以名义16点的两倍,即每em 32像素排版的;在这一尺寸下,它在每个宽度的行数都与Hiragino Sans相同,从8 em的17行到20 em的7行,加上标签后行首出现的也是同样的小写假名。7

8. 此后的像素游戏:文本框、气泡、日语与中文

Stardew Valley:两种说话方式

Stardew Valley同时具备Kiradex已有的两种形式。它的对话框用命令$b翻页,Stardew Valley Wiki的对话模组页面(最后编辑于2026年5月20日)把它描述为“Indicates pauses in dialogue, where the player will need to click for the next part to load in a new dialogue box”(表示对话中的停顿,玩家需要点击,下一部分才会载入新的对话框),肖像命令则“set the portrait shown in the dialogue box for the current line of dialogue”(为当前这句对话设置对话框中显示的肖像)。62 它还有气泡,Strings\SpeechBubbles,用于“dialogue spoken by NPCs through speech bubbles when the player enters a given location”(玩家进入某个地点时NPC以气泡说出的对话)。62 对话框用于有头像在前的交谈;气泡则是你走进来时小镇说的话。

它的非拉丁语言使用位图字体。Wiki关于自定义语言的页面(最后编辑于2026年1月3日)要求新语言把UseLatinFont设为false并提供一个.fnt文件,以“the Chinese, Japanese, Korean, and Russian fonts in the Fonts folder”(Fonts文件夹中的中文、日文、韩文和俄文字体)为范本。它的FontPixelZoom是“A factor by which to multiply the font size. The recommended baseline is 1.5”(字体尺寸的乘数,推荐基准值为1.5),FontApplyYOffset设置“Whether to shift the font up by four pixels”(是否把字体上移四个像素),用于对齐“languages with larger characters like Chinese and Japanese”(中文和日文这类字符较大的语言)。63 1.5倍不是整数倍,但页面把它作为模组作者新增语言的起点,而不是官方中文、日文和韩文字体实际使用的值;而且缩放在屏幕上是否会重新采样,还取决于游戏到设备像素的缩放比例;所以它无法确切说明Stardew如何绘制它自己的日语。63

《最终幻想 像素复刻版》:字体才是真正的工作

像素复刻版的主机版加入了像素字体选项,制作人Naofumi Takuma向Siliconera(Jenni Lada,2023年6月2日)讲述了它是怎么做出来的。西方语言方面,团队找到了公司内部制作的字体数据并加以使用。日语方面:“We didn’t have any alternative data saved on file in-house for the Japanese language font, so we decided to create a new one.”(公司内部没有保存可替代的日语字体数据,所以我们决定新做一套。)以及:“While the Western language font required us to create only a few hundred characters, with the Japanese font, we had to create around 7,500 characters.”(西方语言的字体只需要做几百个字符,而日语字体我们得做大约7,500个字符。)8

这是世界篇的第四处不精确。它给出了同样的数字,“about 7,500 Japanese characters”(大约7,500个日语字符),引用的是GoNintendo对这项工作的报道;一手来源是制作人本人接受Siliconera的采访,原话是“around 7,500 characters”。238 数字相同,来源更好。

七千五百大致相当于JIS X 0208的6,879个字符,也就是通常的覆盖目标;这个比较是我做的。533 一款在发售前每句台词都已定稿的游戏可以画得更少,就像《最终幻想VI》的日文版ROM只用了1,248个对话字母(第3节),但那样每个字符串都得与字体逐一核对;覆盖整个字符集的字体,对其中任何字符串都无需这种核对。4 对一个小应用而言,教训与第4节的免费字体从另一面给出的一样:对于还会不断增长的文字,日语像素字体只有在有人已经画好了七千个字符、且授权允许你发布它们时,才是一个已解决的问题。

现代像素游戏发布了什么

2026年10月5日的Steam上,Stardew Valley列出12种语言,其中有日语、简体中文和韩语;Celeste列出9种,其中有日语和简体中文;Sea of Stars列出11种,其中有日语、韩语以及简体和繁体中文;Eastward列出7种,其中有两种中文和日语。64 这四款每一款都发布了日语。

Celeste、Sea of Stars和Eastward如何绘制其日语和中文文字,我没能在开发者的资料中找到,所以本文除了语言列表之外对此不作任何陈述(第11节)。

Kiradex的两种说话方式

Kiradex已经具备Stardew的两种形式:屏幕底部的WorldHUD文本框,以及广场上头顶的气泡。2625 它缺的是框内那套经典作品的字体排印,以及让VoiceOver能听到两者的方法。气泡代码里的注释说出了它想要的东西:“A line said, as the old games drew it: a cream box with a dark border.”(说出的一句话,就像老游戏画的那样:一个深色描边的奶油色框。)25

9. 技法:带数字的规则

以下是测量所支持的规则,每条都注明出处。凡是把一个测得的事实变成一条建议的规则,那条建议就是我的,规则的措辞会表明这一点。

元素 规则 出处
文本框 文本框占屏幕下方三分之一,两行。放在底部;需要底部的游戏可以把它移到顶部。 《绿宝石》的边框为30×6图块,占满宽度,高度的30%,框内是两行16像素文字的27×4图块窗口;《水晶》为20×6,33%,两行各18个字符,《红》也是同样的20×6;《最终幻想VI》的窗口在第1行或第18行
拉丁字母字形 在8到16像素高的单元格中宽4到12像素。 《绿宝石》的6×16单元格,实际文字中每个字符5.25像素;《最终幻想VI》在16×11单元格中为4到12;《水晶》固定的8×8
日语字形 8到12像素,约为同一字体中拉丁字母的两倍。 Misaki为8×8,着墨7×7;《绿宝石》在16像素的行中宽8;《火红》的字表和PixelMplus10为10;《最终幻想VI》和PixelMplus12为12;测量的每款支持日语的字体中都是8对4、10对5、12对6
行长 每行18到41个字符;按宽度来写,而不是去填满它。 《水晶》为18;《绿宝石》216像素的窗口按5.25像素可容纳41个,其编剧让95%的行停在38个;《最终幻想VI》220像素的一行可容纳18个12像素的日语字符
换行 要么由编剧给每一行换行,要么由代码按单词折行;绝不两者兼用。 《绿宝石》手工换行的24,100行,没有一行超过208像素;《最终幻想VI》的CalcTextWidth和NewLine
速度 以时间计数字符:每秒60个为上限(《绿宝石》和《水晶》的最快设置,以及《最终幻想VI》的每帧一个字母),中档为15到20,慢档为7.5到12。 《绿宝石》7.5、15和60,按住时60;《水晶》和《红》12、20和60;《最终幻想VI》60
翻页标记 让标记以缓慢的节拍移动:每150毫秒一步,一个周期约0.6秒。 《绿宝石》的0、1、2、1像素,每9帧一步,每周期36帧;《水晶》的▼与边框图块交替闪烁
边框 边框是图块,不是绘画:角、边和中心,反复拼接。让玩家从几种中选择。 《绿宝石》可选的20种24×24菜单边框,以及底边镜像的14图块对话长条;《水晶》8种由6个1位图块组成的边框;《最终幻想VI》8种28图块的样式,带7种可编辑颜色
清晰的文字 像素字体只有在每个字体像素为整数个设备像素时才清晰,在Mac上还须关闭字体平滑;在iPhone上截图,看哪一条在那里成立。 macOS 27.0上的Core Text,关闭字体平滑:网格位于基线上、步进为整数像素的字体(Misaki、PixelMplus10、Press Start 2P)在整数倍下为0.0%中间调像素,在整数倍之间为19.4%到82.2%;在上下文的默认平滑下,PixelMplus10以其本身的10像素绘制,272个着墨像素中有178个为中间调
干净的文件 选用字体之前,检查它的网格、与基线的偏移以及步进。 macOS 27.0上的Core Text:PixelMplus12的网格偏离基线0.39像素,每个尺寸都发虚;k8x12的8.01像素假名步进,字形被精确放置后发虚;Silkscreen的10个离群点,1.2%到4.5%
Dynamic Type 通过取整到网格来尊重用户的尺寸,绝不连续缩放,也绝不低于11点。 在3×屏幕上,10像素的em在12档Body尺寸中有1档为整数,12像素的em有3档,8像素的em有2档;HIG的11点最小值
日语换行 文本框文字保持严格的行首规则;预设台词保持足够短,永不折行。 JLREQ 3.1.7;macOS 27.0上的Core Text在不加标签时,对探针的部分行首列表在测量的每个宽度下都保持严格规则,加上ja标签后,在大多数字体中7个宽度里有6个(k8x12和k8x12S中7个里有4个)允许小写假名位于行首
说话人 在台词里写出说话人的名字,而不是放在单独的栏位里或头顶上。 《绿宝石》的对话框没有名字栏,6.5%的段落以名字和冒号开头
覆盖范围 在发布编剧尚未定稿的日语文字之前,先覆盖标准字符集,约七千个字符;定稿的剧本可以少发,但要逐个字符串核对。 JIS X 0208的6,879个;像素复刻版的“around 7,500”;《最终幻想VI》日文版ROM的1,248个对话字母;Misaki、k8x12和DotGothic16画齐了JIS X 0208全部字符,PixelMplus除三个码位外全部画齐
授权 只有在授权文本在手、条款读过之后,才发布一款字体。 Misaki、k8x12和PixelMplus的三句话授权;DotGothic16、Press Start 2P和Silkscreen的SIL Open Font License,以及Press Start 2P的保留字体名
两种形式 两种形式都保留,文本框和气泡。哪种说什么由游戏决定;Kiradex的选择,也是我的选择,是地点说的话用文本框,人说的话用气泡。 Stardew Valley两者兼有:对话框用于交谈,气泡用于玩家进入某个地点时NPC说的话

表中的每个数字都来自第1到第8节,并在那里附有脚注。1467921862

10. Apple的做法:字体、尺寸、换行与VoiceOver

Kiradex World是一个用作2D渲染器的RealityKit场景,正交相机按每纹素整数个设备像素放置,人所阅读的一切都由其上的SwiftUI绘制。23 世界篇决定对话文字“lives in SwiftUI at UI scale rather than in the world”(以UI尺度存在于SwiftUI中,而不是存在于世界里),本节保留这一决定并补全相应的API:Apple对自定义字体的要求,捆绑的像素字体如何接入,如何通过取整来尊重Dynamic Type,日语如何换行,以及VoiceOver如何读取一个由单个RealityKit视图构成的舞台。下文所有的可用版本都取自2026年10月5日保存的Apple文档页面;其中的代码一行都没有构建过,本节的每一项开销都没有在手机上测量过。65

Apple对文字的要求

Human Interface Guidelines的Typography页面给出iOS和iPadOS的默认文字尺寸为17点、最小为11点,并要求应用“Follow the recommended default and minimum text sizes for each platform”(遵循各平台推荐的默认和最小文字尺寸),自定义字体和系统字体都一样。1060 系统字体自身就支持Dynamic Type和粗体文本等辅助功能;对其余字体,页面说:“If you use a custom font, make sure it implements the same behaviors.”(如果使用自定义字体,请确保它实现相同的行为。)10

Accessibility页面定下了工作的规模:“Ideally, give people the option to enlarge text by at least 200 percent (or 140 percent in watchOS apps).”(理想情况下,让人们可以把文字至少放大到200%,watchOS应用为140%。)20 Games页面在此之上加了可读性:“To keep text comfortably legible on each device, ensure that it contrasts well with the background and uses at least the recommended minimum text size in each platform.”(为了让文字在每台设备上都舒适易读,请确保它与背景对比分明,并至少使用各平台推荐的最小文字尺寸。)66

Body的范围14到53点,约为其默认值17的82%到312%;文本框在3×屏幕上取整后的尺寸13.33到53.33点,是其自身默认值16.67的80%到320%,超过了Accessibility页面要求的200%。601133

缩放自定义字体

Apple有三种随Dynamic Type缩放数值的方式,以及一种阻止缩放的方式。

  • UIFontMetrics,“A utility object for obtaining custom fonts that scale to support Dynamic Type”(用于获取可缩放以支持Dynamic Type的自定义字体的工具对象),自iOS 11.0起可用,提供scaledFont(for:)、scaledFont(for:maximumPointSize:),以及“Scales an arbitrary layout value based on the current Dynamic Type settings”(根据当前Dynamic Type设置缩放任意布局数值)的scaledValue(for:)。67
  • SwiftUI中的Font.custom(_:size:relativeTo:)创建一款“that scales relative to the given textStyle”(相对于给定textStyle缩放)的自定义字体,自iOS 14.0起。68
  • ScaledMetric,“A dynamic property that scales a numeric value”(缩放一个数值的动态属性),自iOS 14.0起。69
  • Font.custom(_:fixedSize:)创建一款“that does not scale with Dynamic Type”(不随Dynamic Type缩放)的自定义字体,自iOS 14.0起。68

像素字体需要第一种或第三种与最后一种配合使用:读出系统会使用的尺寸,把它取整到字体的网格,再把取整后的尺寸设为固定尺寸,让系统不再二次缩放。取整本身是算术,见第12节的简报:一个字体像素变成m个设备像素,对于10像素的em,m = max(m₀, ⌊size × displayScale / 10 + 0.5⌋),即半数进一的最近整数,其中m₀是不小于11点的最小m,在3×下为4,在2×下为3,而点数为10 × m / displayScale。在3×下,这把十二档Body尺寸映射为m的4、5、5、5、6、6、7、8、10、12、14和16(15点的Small是唯一恰好为一半的情况:4.5进为5):默认尺寸下为16.67点,AX5下为53.33点。11

要在视图内限制尺寸,dynamicTypeSize(_:)(iOS 15.0)设置或限定Dynamic Type尺寸,其页面建议:“When limiting the Dynamic Type size, consider if adding a large content view with accessibilityShowsLargeContentViewer() would be appropriate.”(在限制Dynamic Type尺寸时,考虑是否适合用accessibilityShowsLargeContentViewer()添加一个大内容视图。)70 这个修饰符同样是iOS 15.0,它自带一条警告:“Don’t use the large content viewer as a replacement for proper Dynamic Type support.”(不要用大内容查看器替代正确的Dynamic Type支持。)71 简报只限制必须保持一行的气泡,从不限制文本框。

捆绑字体

字体随应用包发布,并在UIAppFonts中列出,即“App-specific font files located in the bundle and that the system loads at runtime”(位于应用包中、由系统在运行时加载的应用专用字体文件,iOS 3.2),或者在代码中用CTFontManagerRegisterFontsForURL(_:_:_:)(iOS 4.1)注册。7273

绘制边框与放置基线

由几个图块绘制而成的边框,就是一张不经平滑缩放的图片。SwiftUI的Image.interpolation(_:)设置插值质量,自iOS 13.0起;Image.resizable(capInsets:resizingMode:)“Sets the mode by which SwiftUI resizes an image to fit its space”(设置SwiftUI调整图片大小以适应其空间的方式),拉伸或平铺除四角外的一切,同样自iOS 13.0起。7475 简报改为在一个Canvas中绘制边框,让每个源像素恰好是m×m个设备像素;依我的解读,带.tile的resizable(capInsets:)只有在其平铺落在整数设备像素上时才同样可用,而这一点没有人测量过。

Text.baselineOffset(_:)“Sets the vertical offset for the text relative to its baseline”(设置文字相对于其基线的纵向偏移,iOS 13.0),是用于网格偏离基线的字体的工具,PixelMplus12正是如此;第6节测得,偏移只能在一个尺寸下修正这类字体,这也是简报不用它的原因之一。767

SwiftUI与UIKit中的日语换行

typesettingLanguage(_:isEnabled:)(iOS 17.0)用于“may contain text of a particular language which doesn’t match the device UI language. In that case it’s useful to specify a language so line height, line breaking and spacing will respect the script used for that language”(可能包含与设备界面语言不一致的某种语言文字的Text。这种情况下,指定一种语言会很有用,这样行高、换行和间距都会遵循该语言所用的文字系统)。其页面补充道:“this language does not affect text localization.”(这种语言不影响文字的本地化。)77

UIKit的NSParagraphStyle.LineBreakStrategy(iOS 9.0)有三种命名策略。standard(iOS 14.0)“uses the same configuration of line-break strategies that it uses for standard UI labels”(使用与标准UI标签相同的换行策略配置),并且“may be unsuitable for large amounts of text”(可能不适合大量文字);pushOut(iOS 9.0)用于避免“an orphan word on the last line”(最后一行孤零零的一个单词);hangulWordPriority(iOS 14.0)用于韩语。78

第7节测得,macOS上的Core Text在加上ja标签后,在大多数字体中7个宽度里有6个(k8x12和k8x12S中7个里有4个)允许小写假名位于行首,不加标签则不允许。7 当设备语言为日语时SwiftUI的Text如何处理日语,以及typesettingLanguage设置的是否就是测量中设置的同一属性,都没有测量(第11节)。因此,除非截图显示加上标签后严格规则仍然成立,简报都让文本框文字保持不加标签。

舞台上的VoiceOver

SwiftUI的辅助功能修饰符描述的是视图:accessibilityLabel(_:)为它命名,accessibilityValue(_:)添加它的值,其文档中有一条规则:“Use this method to describe the value represented by a view, but only if that’s different than the view’s label.”(使用此方法描述视图所代表的值,但仅限于它与视图标签不同时。)以页面列出的LocalizedStringResource形式,两者都是iOS 16.0。79

游戏的舞台需要的不止是一个标签。accessibilityDirectTouch(_:options:)(iOS 17.0)建立一个区域,在用户激活后,“Direct touch areas passthrough touch events to the app rather than being handled through an assistive technology, such as VoiceOver”(直接触摸区域会把触摸事件直接传给应用,而不是交由VoiceOver等辅助技术处理)。80 AccessibilityTraits.updatesFrequently(iOS 13.0)用于会变化的元素:“Use this trait when you want an assistive technology to poll for changes when it needs updated information.”(当你希望辅助技术在需要最新信息时轮询变化,就使用这个特征。)81 AccessibilityNotification.Announcement(iOS 17.0)发出一条朗读通知,并带有一个accessibilitySpeechAnnouncementPriority属性,用来设置它与其他语音之间如何排队。82 而accessibilityReduceMotion(iOS 13.0)报告Reduce Motion设置,其页面说,这意味着“UI should avoid large animations”(界面应避免大幅动画)。83

RealityKit可以描述实体本身。AccessibilityComponent(iOS 17.0)是“A component that stores accessibility information for an entity”(为实体存储辅助功能信息的组件),其名称是“VoiceOver says aloud when it describes the entity”(VoiceOver在描述该实体时朗读出来的)。84 Apple的文章《Improving the Accessibility of RealityKit Apps》说要“set the properties on each visible Entity in your scene”(为场景中每个可见的Entity设置属性):即Entity自身的辅助功能属性isAccessibilityElement、accessibilityLabel和accessibilityDescription,文章把它们标为iOS 14起,而除isAccessibilityElement外,它们的页面现已标为弃用(页面还列出了accessibilityLabelKey,即以本地化字符串键表示的标签),这是与组件并存的一条较旧的路径。85 Kiradex舞台所用的SwiftUI视图RealityView是iOS 18.0。86 一个像Kiradex那样设置了虚拟正交相机的RealityView,是否会把这些实体暴露给VoiceOver,这些页面没有说明,也没有测试过(第11节)。

现在测试它可以自动化了。XCUIVoiceOverService“Provides programmatic control of VoiceOver for UI testing”(为UI测试提供对VoiceOver的程序化控制),自iOS 27.0起可用,通过XCUIDevice上的voiceOverService属性获取。87

Kiradex的现状

以下读自2026年10月5日的代码仓库,提交334fbfe加上当天未提交的修改,我没有运行或改动过;我阅读时应用正在被修改(第11节):11

  • 只有一款字体,系统的。 主题的字体家族是.systemDesign(.monospaced),世界的视图使用套件的角色:caption在四个文件中有10处调用,label 3处,body 3处,title 3处。2411
  • 一条连续缩放的自定义字体路径。 共享套件的Kit941.FontFamily.custom(named:)解析为Font.custom(_:size:relativeTo:),display、title、body、label和caption分别为34、22、17、13和12点。88 换成像素字体,它就会随每个Dynamic Type尺寸缩放,按第6节的计数,12像素的em在12档Body尺寸中有9档偏离网格,10像素的em有12档中的11档。7
  • 气泡。 Text(text).kit941Font(.caption, weight: .bold),以墨色绘于WorldInk.paper(一个有两种固定颜色的美术表面)之上,左右留套件的small间距、上下留extra-small间距,带2点墨色描边和.fixedSize(),所以只有一行。它放在rig.headPoint上方一个large间距处,在台词出现后不满4秒期间显示,并在一个关闭了命中测试的叠加层里带有标签"Said: \(text)"。这个叠加层位于舞台内部,在舞台覆盖整个场景的两个叠加层之下:扫描线,每3点一条16%不透明度的黑线,以及暗角。2511
  • 文本框。 WorldHUD把地点的标题(caption,粗体,大写)和状态(caption,半粗体)放进一个同样带2点描边的奶油色文本框,置于屏幕底部;在Duo半开时,它停在折痕较宽的一侧。它不接受点按(.allowsHitTesting(false),“Taps go through to the ground under it”(点按会穿透到它下方的地面)),并覆盖在整个舞台之上,位于扫描线和暗角之上。26
  • 说出的话。 服务器上有23句预设台词,3到40个字符,中位数16;最长的一句是“Anyone have a card from the first years?”。昵称由各含20个单词的两个列表中各取一词加四位数字组成,最多20个字符(“Glacier Shuffle 0000”)。收藏者头顶上除了气泡什么也不画;昵称显示在点按后打开的面板里。288911 本系列关于小镇自身生活的文章统计的也是这23句台词,以及服务器对每位玩家每2.0秒一句的限制。9091
  • 没有字体,只有一种语言。 应用没有捆绑任何字体文件(仓库中唯一的.ttf是给截图工具用的scripts/screenshots/MartianMono-latin.ttf);项目的knownRegions是Base和en,也没有.xcstrings或.lproj文件。1127
  • VoiceOver听到的是坐标。 舞台是一个元素,.accessibilityLabel("The plaza")和.accessibilityValue(stepTrace),其中stepTrace以“x,y facing”的形式记录最后一步,保留它是因为“the walks read it, since nothing else the simulator logs reaches them”(行走测试要读它,因为模拟器记录的其他任何东西都传不到它们那里)。AccessibilityComponent、accessibilityDirectTouch、AccessibilityNotification、typesettingLanguage、UIFontMetrics、ScaledMetric和dynamicTypeSize在应用源码中一处都没有出现。2511
  • 没有文字的招牌。 锻炉的sign()是“A hanging sign on a bracket, 12 × 11”(挂在托架上的12×11招牌),招牌面上是两行墨迹,不是文字;广场地图上有一个招牌图块S。手指按在地面上,玩家会立即走向手指下方的图块,并跟随手指跨越图块;点按一位收藏者,会在手指抬起时打开其面板,点按信息亭则打开小镇的公告板;招牌图块与其他地面无异,没有自己的点按。2992
  • 世界的缩放。 按PlazaRig.fit的规则,即视图像素高度除以320后向下取整,一个纹素在iPhone 18 Pro Max(440×956点)和Duo内屏竖屏时为8个设备像素,在Duo外屏竖屏和内屏横屏时为6个,在外屏横屏时为4个:即2.67、2.00或1.33点。9311

为什么文字留在世界之外

世界篇在这一点上的算术是对的:“At k = 8 on a 3× display a five-texel font is 13.3 points and at k = 6 it is 10 points, under the HIG’s 11-point minimum”(在3×屏幕上,k = 8时五纹素的字体为13.3点,k = 6时为10点,低于HIG的11点最小值)。23 它只约束k = 6时小于5.5纹素的字体(每纹素2点,即11点);k = 6时8纹素的em为16点。1133 文字以UI尺度放在SwiftUI中这一结论之所以成立,原因另有所在:在世界的密度下,在每纹素8个设备像素的手机上,10像素的em为26.67点,这使得18 Pro Max上文本框的一行只剩29个拉丁字母。11 以图块来量,经典作品的对话框很窄。一个16个硬件图块宽、即128像素的框,能放16个8×8字体的字符,约24个按《绿宝石》平均5.25像素计的拉丁字母(按最坏的6像素为21个),16个《绿宝石》8像素的日语字符,以及10个《最终幻想VI》12像素的日语字符。Kiradex的16个16纹素图块是256纹素,在世界本身的密度下能放42个拉丁字母和21个PixelMplus12的假名,若用Misaki则为64个和32个。11 而以Kiradex自己的图块来量的文本框,在测量的五个视图中有三个放不下:16个16纹素的图块,在每纹素8个设备像素时为683点,比iPhone 18 Pro Max(440点)和Duo内屏竖屏(669)都宽;在6个时为512点,比Duo外屏竖屏(466)宽。它只放得进两个横屏视图:内屏的951中放512点,以及在每纹素4个设备像素时,外屏的678中放341点。11 Kiradex的文本框以点而不是以图块来定尺寸。

按简报的尺寸做同样的算术:PixelMplus10在m = 5、16.67点时,在竖屏18 Pro Max的文本框中,从整个宽度减去目前48点的边距(每侧16点外边距加8点内边距),并假设左右没有安全区域边距(这是假设,没有测量),每行可容纳47个拉丁字母或23个日语字符。文本框位于安全区域内,所以在Duo外屏竖屏时,字符数取决于其左右边距:没有边距时为50或25个,若有本系列在合上的屏幕上测得的84点尾侧边距,即侧边轨,则为40或20个(第11节)。在简报的边框下(第12节第3项),边框以每侧8个字体像素取代内边距,并把文本框向下取整到整数字体像素,同样的一行在18 Pro Max上可容纳45或22个,在Duo上为48或24个,有侧边轨时为38或19个。119495

开销

这些都没有在设备上测量过。捆绑一款7,253个字形的字体会让应用增加这个文件的大小;7 用自定义字体通过SwiftUI绘制文字,开销与任何Text相同;简报的边框是一个在尺寸变化时重绘的Canvas。依我的解读,与一个RealityKit场景相比,这些都不算大,但这只是解读,要由简报的截图来检验。

11. 测量了什么,没有测量什么

本文的每个数字,要么来自针对已保存来源可重新运行的脚本,要么来自2026年10月5日保存的页面,要么来自注明日期的Kiradex源码阅读;脚注写明是哪一种。本节列出这些所没有覆盖的内容。30

从来源测得、可重新运行的部分。 measure_gen_text.py针对四个pret反编译:《绿宝石》的字体表(十项,七款字体);以帧和每秒字符数计的文字速度;滚动;翻页箭头;27×4的窗口和30×6的边框;20种可选边框;拉丁字母字形宽度;以字符和像素计的24,100行剧本;14,412个段落中写出说话人的934个;日语字形字表;《火红》的延迟和窗口;《水晶》和《红》的屏幕、对话框、延迟、边框和字体字表。1 measure_ff6.py针对ff6反汇编:窗口样式和调色板,对话字体的单元格和宽度,日语字体的大小和宽度分段,220像素的一行,每帧一个字母,100帧的标题,按单词折行,设置画面,以及窗口的渐变表。4 measure_bdf.py针对Misaki和k8x12的BDF文件。6 measure_coretext.swift和measure_outline_coords.swift在macOS 27.0上编译运行,针对十个TrueType文件和Hiragino Sans。719 measure_kiradex_text.py针对提交334fbfe时磁盘上的Kiradex(10月15日针对当前代码树重新运行,原有各行不变),measure_figure_width.py针对其身体字表。1196 measure_hig.py、measure_steam_languages.py和measure_apple_docs.py针对已保存的页面。606465 measure_ios27_iphones.py针对iOS 27.0模拟器运行时的iPhone列表,probe_line_start_glyphs.swift在macOS 27.0上编译运行,针对JLREQ的分类表和PixelMplus10。9798 measure_cell_masks.py针对已保存的pret字表:即简报的单元格掩码参照。99 以上共十三个脚本;第十四个dump_glyphs.swift与本文的配图放在一起,以开启和关闭字体平滑两种方式按PixelMplus10本身的尺寸绘制了它。9

没有测量的,或比看上去更弱的部分。

  • 用macOS上的Core Text代替iOS。 第6节和第7节的每个渲染和换行数字,都来自macOS 27.0上的Core Text在关闭字体平滑的情况下绘制到位图上下文,而不是iPhone上的SwiftUI。真正算数的测量是简报的截图检查;在它们运行之前,iPhone画出什么只是预测。79
  • 位图点阵。 Misaki和k8x12带有EBDT和EBLC表;在8和12像素下Core Text画的是这些位图还是轮廓,没有确定。7
  • SwiftUI自身的放置。 SwiftUI是否把Text的原点放在整数设备像素上,以及当设备语言为日语时它对日语采用哪种换行规则,都没有测量。
  • 字重。 PixelMplus10 Bold是唯一测量过的粗体;任何像素字体的合成粗体都没有测量。7
  • 可读性。 网格和覆盖范围测过了;10×10的汉字在16.67点下对日语读者是否易读,没有测量,需要日语读者在设备上看过才行。
  • 日版ROM。 pret构建的是英文版游戏。日版《绿宝石》本身的对话框、速度和字体都没有读;第1节中的日语字表,是英文版ROM为名字而携带的那些。日版《水晶》和《红》也没有读。30
  • 《最终幻想VI》的图像。 反汇编里有的是范围和代码,不是像素;窗口图块的排布和渐变的样子都没有看到。英文版大字体的范围5,120字节不是22字节字母的整数个,原因不明。渐变表每条扫描线消耗一个值,来自注释,没有追踪。4
  • 《水晶》的闪烁周期没有推导;它的两种状态是从代码中读出的。16
  • **《时空之轮》**完全没有测量。没有克隆任何公开的反汇编,Data Crystal的ROM地图返回了HTTP 403。49
  • Celeste、Sea of Stars和Eastward。 只有它们的Steam语言列表;它们如何绘制日语和中文文字,没能在开发者资料中找到。64
  • Pixel Operator、m5x7和m3x6都没有下载或测量,Pixel Operator自己的仓库返回了HTTP 404;它们的授权以各自页面所述为准,Pixel Operator的仅来自DaFont。565758
  • PixelMplus未映射的三个JIS码位没有在其替代码位上核查。7
  • DotGothic16的尺寸。 它没有像素网格,所以是按名义上的每em 16点及其倍数来绘制和排版的,这是我根据它的名字和点距做的选择;没有尝试其他名义尺寸,它在第6节和第7节中的数字只适用于这些尺寸。719
  • 字体平滑。 第6节的渲染全都不允许字体平滑;使用位图上下文的默认设置时,只画了一款字体的一个尺寸,即10像素的PixelMplus10,272个着墨像素中有178个是中间调。其他字体和尺寸没有在开启平滑的情况下绘制。9
  • 我阅读时应用在变化。 撰写本文的10月5日,Kiradex正在被修改。当天早些时候,舞台的点按处理被重写,其代码行移动了30到144行,世界的Swift文件中有三个还没有进入任何提交;我阅读后十四分钟,舞台和骨架又改了一次。本文读的是提交334fbfe加上我阅读时当天未提交的修改:应用的文件在太平洋时间12:28(服务器的收藏者和小镇地图在12:33),测量它们的脚本在12:34和12:36。脚注给出了那次阅读时每个文件的行号;简报以符号而不是行号来指称,我在12:42核实了它所指称的每个符号都还在。11
  • 应用运行的iPhone。 33款iPhone的列表(其中三款为2×)是iOS 27.0模拟器运行时的,读自我Mac上的Xcode;Apple关于iOS 27支持哪些iPhone的页面没有获取。97
  • 安全区域边距。 本系列关于iPhone Duo模拟器的文章测量了合上状态的外屏,宽466点,其内容在84点的尾侧边距,即侧边轨之内宽382点,那次探针显示了状态栏、工具栏和标签栏。95 Kiradex的世界隐藏了状态栏和主屏幕指示器,也不显示任何栏;代码把世界的界面元素放在安全区域内,并说这样能避开摄像头和侧边轨,但无论是代码还是那次测量,都没有说明隐藏状态栏后边距有多宽,所以第10节和第12节中Duo的数字给出了两种情况:没有左右边距,以及测得的84点。其他iPhone竖屏时的左右边距没有测量;简报中针对它们的数字假设没有边距。94
  • Kiradex配置下RealityKit的辅助功能。 AccessibilityComponent的文档标为iOS 17.0;带虚拟正交相机的RealityView是否会向VoiceOver暴露实体元素,尚未核实。84
  • 简报中的数字。 m的取整,比文本框小一档并受每个视图上限约束的气泡,32点的边距,文本框的队列及其三句台词,以边框的8个字体像素取代文本框内边距,150毫秒的标记步进,四种边框,以及播报的六格范围,都是提议,不是事实。

本文与世界篇的不同之处。 第1、2、5和8节各纠正了它的一处陈述,并给出测量;它的尺寸算术是对的(第10节)。本文从系列其他文章中采用的三个事实,已与应用核对:小镇的23句预设台词与lines.py一致;角色篇所说气泡仍按单元格高度定位,与PlazaRig.headPoint一致,后者在行走者的位置上加上字表帧高度的一半再加4;气泡下的人物正是那篇文章更新中描述的那个,在32×40的单元格中高30纹素,应用的清单、rig.py和身体字表都证实了这一点。那篇文章只给出了它早先26纹素人物的宽度,肩宽约14;第12节从字表中测量了当前人物的宽度。90100289396

没能获取的来源。 Data Crystal的《时空之轮》ROM地图(HTTP 403);Pixel Operator在NotABug上的仓库(HTTP 404);m5x7和m3x6的字体文件(在“自定价格”的下载之后,没有尝试);以及关于Celeste、Sea of Stars或Eastward日语和中文文字的开发者资料(没有找到)。49565758

12. 简报:Kiradex要做什么,以及必须通过的检查

此后应用又往前走了。 到10月15日的提交fc9d4876,舞台的标签和值改在一个StageAccessibility视图修饰符中设置(PlazaStage.swift,在第373行应用,从第520行开始定义),标签仍是“The plaza”,值仍是步伐记录,而当某个地点覆盖舞台时该元素不存在。下面的检查读的是10月5日的构建,所有“现在”或“今天”都指那个构建;第8项对两者都适用。101

作者综述。 以下是我的提议,依据第1到第11节,写成相对于2026年10月5日Kiradex的差异,即提交334fbfe加上当天工作树中未提交的修改(第11节):世界每个视图中的系统等宽文字,caption粗体的单行气泡,底部的WorldHUD文本框,服务器上存放英语预设台词的纯英语应用,没有文字的招牌精灵,以及VoiceOver读作坐标的舞台。2211 世界篇的方向不变:文字即美术;对话以UI尺度存在于SwiftUI中,而不在世界的纹素里;文本框由图块构成;两行;打字机;翻页标记。23 改变的是字体,原因见第5节,以及算术,因为Dynamic Type必须取整到字体的网格。7 这里不绘制任何系列作品的字形、边框、标记或词语;一切都由锻炉绘制,或用一款在第4节引用了授权的字体排版。法律方面的说明是我的解读,不是法律意见。这些都还没有做。

下面每一项检查,都可以由脚本、测试的一行日志或一张截图判定通过或失败,只有第8项中一项注明日期、在设备上手动进行的检查例外。凡是检查结果取决于设备、文字尺寸、所选台词或文本框中还显示着什么的,都会写明。简报以符号而不是行号来指称应用的代码,因为我阅读时代码在变动;脚注给出我阅读时的行号,以及提交和时间。凡是某一项的依据是我在Mac上的测量、而检查在iPhone或其模拟器上运行的,由检查而不是Mac来定论。

1. 两种语言共用一款字体:PixelMplus10

内容。 通过UIAppFonts捆绑PixelMplus10-Regular.ttf和PixelMplus10-Bold.ttf(itouhiro制作,源自M+ FONTS PROJECT的M+ BITMAP FONTS;授权:M+ FONT LICENSE,https://github.com/itouhiro/PixelMplus/blob/master/misc/mplus_bitmap_fonts/LICENSE_E),在应用包中放入LICENSE_E,并在目前没有致谢部分的设置中新增一个致谢部分,写明itouhiro和M+ FONTS PROJECT。1872 在世界之上绘制的每个字都用它:气泡、WorldHUD文本框、招牌文字、任务公告板和展示铭牌。应用的其余部分,即图鉴、扫描器和设置,保留系统字体。

为什么是这一款。 测量过的字体中,有两款具备第9节规则所要求的全部三点:日语、干净的网格和整数像素步进。Misaki Gothic和Misaki Gothic 2nd在位于基线上的8像素网格上画齐了JIS X 0208全部6,879个字符,样本轮廓坐标中偏离网格的为848个中0个和860个中0个,步进为4和8。PixelMplus10在位于基线上的10像素网格上画出了6,879个中的6,876个,包括两级水准的每一个汉字,Regular的1,032个样本轮廓坐标中偏离网格的为0个,Bold的1,000个中为0个,拉丁字母步进为5,假名为10。7 简报选择PixelMplus10,这是我的选择:它是两者中较大的,10像素对Misaki的8像素,而Misaki的8×8汉字正是对日语读者而言可读性最存疑的(第11节);而且它带有测量过的Bold,粗体文字不需要合成。被排除的,每一款都附上排除它的测量:

  • PixelMplus12(M+ FONT LICENSE):其网格位于基线下方0.39像素,macOS 27.0上的Core Text在每个尺寸下都让它发虚。7
  • k8x12(littlelimit.net上的三句话授权,https://littlelimit.net/font.htm#license):其假名步进为8.01像素,其拉丁字母宽4像素。717
  • Misaki(同一授权,https://littlelimit.net/font.htm#license):三点都具备,但它是测量过的最小的汉字,8×8,也是对日语读者而言可读性最存疑的(第11节)。617
  • DotGothic16(SIL Open Font License 1.1,https://github.com/google/fonts/blob/main/ofl/dotgothic16/OFL.txt):它的点不落在任何网格上,macOS 27.0上的Core Text在16到64像素下把它绘制为37.3%到82.3%中间调。753
  • Press Start 2P(带保留字体名的SIL Open Font License 1.1,https://github.com/google/fonts/blob/main/ofl/pressstart2p/OFL.txt)和Silkscreen(SIL Open Font License 1.1,https://github.com/google/fonts/blob/main/ofl/silkscreen/OFL.txt):没有日语。75455
  • m5x7(其页面:“Creative Commons Zero v1.0 Universal”,https://managore.itch.io/m5x7)和m3x6(其页面:“free to use with attribution”,没有授权一栏,https://managore.itch.io/m3x6):没有测量;按其页面自己的描述只有拉丁字母;在“自定价格”的下载之后。5758
  • Pixel Operator(据作者在DaFont上的说明为CC0 1.0,https://www.dafont.com/pixel-operator.font):没有测量;DaFont上为241个字形,对日语来说远远不够。56

套件。 我的各个应用共用的套件把每种文字样式都路由到.kit941Font(role),其自定义字体路径会连续缩放字体,这会让这款字体在12档Body尺寸中有11档偏离网格,而这正是Mac上Core Text让它发虚的条件。887 世界本来就是一个已声明的美术表面WorldInk,有自己的两种颜色。25 新增一个Kiradex本地的PixelText修饰符,以注释声明为该美术表面的例外,它接收一个套件角色并应用第2项的取整;等它上线之后,才向上游提议成为套件字体家族的一种情况。

不用合成字重。 粗体文字使用PixelMplus10-Bold;不对像素字体使用.weight(_:),因为Core Text对必须合成的字重会做什么,没有测量过(第11节)。

检查。 - 一项单元测试,由于iOS上也有Core Text,它加载两个捆绑文件并断言:每em 1,000单位;世界的字符串和预设台词在两种语言中用到的每个字形,其步进都是100单位的倍数;样本字形的轮廓点都落在100单位网格的4单位以内(即一个字体像素的0.04,与第4节探针所用的容差相同)。不通过的字体不能发布。 - 一项测试,确认世界可能显示的每个字符串(预设台词、String Catalog中的世界键、招牌文字)都能映射到该字体中的字形,没有后备字体。 - 一个构建阶段脚本,在构建出的应用包中找到LICENSE_E;以及一项UI测试,在设置新的致谢部分中找到字体的名称、作者和授权。

2. 尺寸:取整到网格的Dynamic Type

内容。 一个字体像素始终等于整数m个设备像素。对每个角色,读出系统会使用的Dynamic Type尺寸(UIFontMetrics(forTextStyle:).scaledValue(for:),或对该角色的基准尺寸使用@ScaledMetric(relativeTo:)),把这个尺寸取整到百分之一点,使一个比表中尺寸略小一丝的缩放值,比如15对应的14.999…,能像表中尺寸那样取整;然后设m = max(m₀, ⌊size × displayScale / 10 + 0.5⌋),即半数进一的最近整数,其中m₀ = ⌈11 × displayScale / 10⌉是不小于11点的最小m,pointSize = 10 × m / displayScale,再用Font.custom(_:fixedSize:)应用它,让系统不再二次缩放。676968 文本框的标题和文字从目前使用的caption角色改为body角色,这样在3×下,文本框把十二档Body尺寸映射为m = 4、5、5、5、6、6、7、8、10、12、14和16:默认尺寸下为16.67点,AX5下为53.33点。2611 最小的3×下m₀ = 4,即13.33点,高于11点的最小值。1033 在iOS 27.0模拟器为三款iPhone列出的2×下(第4项),m₀为3,即15点,十二档Body尺寸给出3、3、3、3、4、4、5、6、7、8、9和11:默认尺寸下为15点。9711 气泡小一档,绝不低于m₀,也绝不高于每个视图设定的上限(第4项)。

放置。 像素字体只有在其基线落在设备像素上时才清晰(第6节)。把世界叠加层中计算出的每个原点都取整到设备像素网格,(x × scale).rounded() / scale,包括PlazaStage.bubbles的.position,并给文字视图以整数设备像素的宽度,给文本框以整数字体像素的宽度和高度(第3项)。

行距。 PixelMplus10向上9个字体像素,向下2个;把行距设为12个字体像素,两行之间空一行,正如《绿宝石》15像素的日语字形和16像素的拉丁字母单元格放在16像素的一行里。71 在m = 5时,即每行20点。33

检查。 - 对取整这一纯函数进行测试,覆盖显示比例2和3下的每一个DynamicTypeSize:结果是每个字体像素为整数个设备像素,绝不低于11点,尺寸越大结果绝不越小。测试直接把HIG表中的Body尺寸喂给函数,而不是从scaledValue(for:)读取的值,后者的确切输出我没有见过:在3×下,十二个尺寸给出上面的十二个值,Small的4.5进为5,在2×下给出十二个2×的值;每个尺寸减去十亿分之一点,经过取整到百分之一,得到同样的m。11 - 在iPhone 18 Pro Max模拟器上以Large、xxxLarge和AX3截取带有一个气泡和文本框的广场,在2×的iPhone SE(第3代)模拟器上以Large截取,并且已把气泡叠加层移到舞台扫描线和暗角叠加层之上(第4项);再用一个脚本裁出文字,按墨和纸两种颜色统计中间调像素:字形框内、气泡中和文本框中都为0。每张截图都需要运行广场服务器(KIRADEX_PLAZA=1),并在玩家从“Say”菜单说出当前最长的台词后4秒内截取,因为每句台词都是从房间返回后才显示102,而气泡只在台词出现后不满4秒期间显示。25103 同一个脚本在当前构建上运行,记录它要取代的基线。第6节的0.0%是Mac上关闭字体平滑的Core Text;这项截图才是判定iPhone是否画得一样的检查。

3. 文本框:锻炉绘制的边框、两行、翻页

内容。 WorldHUD的文本框保持原位:底部,安全区域内,Duo半开时位于折痕较宽的一侧。26 它那2点描边的矩形改为锻炉绘制的边框:frame-paper.png,24×24字体像素,3×3个8像素图块(角、边和中心),使用WorldInk的两种颜色外加一种阴影色,为Kiradex原创。一个PixelFrame视图在Canvas中绘制它,每个源像素都是m×m个设备像素的正方形:四个角放角图块,边图块和中心图块从左上角向外重复,最后一块在碰到对边处截断,这样边框的像素就是文字的像素。为了让每个源像素都是完整的正方形,文本框的宽度取WorldHUD目前布局时所用的安全区域宽度,每侧减去16点,向下取整到整数字体像素,即m个设备像素的倍数;高度也取整数字体像素:在竖屏18 Pro Max上、m = 5、左右没有边距时(这是假设,没有测量),1,224个设备像素为244.8个字体像素,向下取整为1,220,即244个字体像素。1194 文字位于边框之内,距文本框各边8个字体像素,以取代目前的8点内边距(我的选择),这样每行剩下228个字体像素,即45个拉丁字母或22个全角字符。11 地点的标题在每一页的顶部独占一行;其下的文字一次显示两行,就像《绿宝石》和《水晶》那样。1 超过两行的文字翻页,每一页都在翻页标记下等待(第6项);如果一页在空格处结束,这个空格就丢弃。WorldHUD的box(在WorldHUD.swift中)接收这些页面,以取代它目前的单个状态字符串。

队列。 这是我的规则,为的是让文本框显示的内容不取决于状态有多长:

  • 状态的页面是文本框自己的。当状态变化时(在广场里,就是其中任何东西变化时:连接状态、在场人数、一天中的时段、季节、获得的绶带),它的页面会被替换,文本框从第一页开始显示新的状态,除非正在显示下一条规则中的页面。
  • 招牌的文字(第5项)和大字号下在附近说出的台词(第4项)是排在状态之前的页面:立即显示,各自排在已在等待的此类页面之后,按到达的顺序,在最后一页之后,再从第一页开始显示状态。文本框最多保留最近三句台词的页面;当第四句到来时,丢弃仍在等待、不属于玩家本人的最早那一句(只有在等待的每一句都是玩家本人的时,才丢弃玩家本人最早的那一句),所以说了一句话的玩家永远不会丢失它。新的招牌会替换仍在等待的招牌页面。
  • 状态文字无论显示哪一页,都像现在一样把整个状态作为其辅助功能标签,这样VoiceOver会完整地读出它,而现有读取plaza-status、room-status和hall-status的UI测试(开头的短语、在场人数、房间的头几个词)读到的仍与现在一样。当前显示的那一页,已显示出的部分,作为一个隐藏元素的值,在广场里是plaza-page(<name>-page,名称由我拟定)。它包含该页去掉地点标题后的文字行,并且只在行走测试的启动参数-worldTrace下存在(第8项);下面的检查读取它,并且每项都传入-worldTrace。103

可选的边框。 《绿宝石》让玩家从20种中选择菜单的边框(其对话框始终只有一种边框),《最终幻想VI》让玩家从8种中选择窗口并可编辑颜色。14 在设置中提供四种锻炉绘制的边框,全部免费,前三种一开始就有,第四种在达到某个等级时解锁;边框永不出售。是否把某种边框作为给支持者的回报,我还没有决定。

检查。 一项锻炉检查,确认frame-paper.png和每种备选边框都是24×24、只使用调色板索引,并且在设计要求之处对边完全相同;在iPhone 18 Pro Max模拟器上,以m = 4(xSmall)和m = 5(默认尺寸)截取文本框,由脚本确认文本框以设备像素计的宽度和高度都是m的倍数,并把它的边框,即每侧最外的8个字体像素,与脚本自己按同一规则(角图块放在四角,边图块和中心图块从左上角重复并在对边截断)以截图中文本框的尺寸从PNG合成的边框逐像素比较,每个源像素为一个m×m的正方形,忽略PNG中透明、透出世界的像素;以及在同一模拟器的默认尺寸下、不运行广场服务器以免有人说话的一项UI测试,以-worldTrace和-worldBoxSample启动,并给出一个由换行符连接的三行状态,每行30到40个字符,都在那里每行可容纳的45个字符之内,测试等待plaza-page在忽略空白后等于前两行,然后才在第一页完整显示后点按文本框,找到第三行,并截取第一页,由脚本在文本框右下角找到标记。11 -worldBoxSample是由我命名的测试参数,像-plazaTell(其tellOnArrival)一样在PlazaView中读取,并作为状态交给WorldHUD,以取代广场自己的状态。92

4. 气泡:一行、不显示名字、两种语言

内容。 保留气泡的形式:奶油色,墨色描边,在头顶上方,不满4秒。25 用像素字体以气泡的m排版,用边框的角像素代替描边矩形,画在气泡每侧8点的内边距之内,这样就像现在的描边一样不增加宽度。气泡里和上方都不显示名字:说话人就是气泡所在的那个头,由VoiceOver说出他们的名字(第8项)。预设台词永不折行;.fixedSize()保留。

移到扫描线之上。 现在气泡是舞台内部的一个叠加层,位于舞台的扫描线(每3点一条16%不透明度的黑线,在3×下即每9个设备像素中有3个像素宽的一条)和暗角之下,而文本框则覆盖在整个舞台之上。2526 在这些层之下,气泡的纸面会穿过字形被一条条压暗,所以在任何尺寸下它的文字都不是纯粹的墨与纸。简报把气泡叠加层移到扫描线和暗角叠加层之上,也就是文本框已在的位置,而不是把第2项的检查只限于文本框:这是我的选择,依据的是第6节的规则,即像素字体只有作为墨与纸的整数设备像素才清晰,对两种说话方式一视同仁。在PlazaStage.swift中,气泡的.overlay是舞台GeometryReader内的最后一个修饰符,把它移到暗角的.overlay(RadialGradient)之后,并给它一个自己的GeometryReader来获取视图尺寸。25

留在屏幕上。 现在气泡居中于说话人头顶上方,没有任何东西让它保持在屏幕内,所以靠近边缘的说话人会把它的一部分推出屏幕。25 把气泡的框限制在安全区域每侧减去16点的范围内,也就是下面宽度规则留出的边距,只在限制需要时才把它从头顶中心移开。是安全区域,不是视图:舞台忽略安全区域(.ignoresSafeArea()),所以它自己的几何范围延伸到安全区域所避开的东西之下,在Duo上就是摄像头和侧边轨,WorldHUD的注释也是这样说它自己在安全区域内的位置的。94 现在没有任何东西把边距传给舞台。由PlazaView在舞台之外读取边距并传进去,就像CardViewer已经为它的卡片所做的那样(把proxy.safeAreaInsets传入CardStage);限制是一个纯函数BubblePlacement.frame(head:size:view:insets:),从PlazaStage.bubbles(of:in:)调用,同样的边距也决定下面气泡的上限(这些名称是我的)。世界隐藏了状态栏和主屏幕指示器(在根视图上使用.statusBarHidden()和.persistentSystemOverlays(.hidden));这是否会缩小边距,代码中没有体现,所以应用读取边距,而不是假设它们。94

宽度规则。 每句台词,两种语言都算在内,在应用能运行的每一款iPhone上,在安全区域没有左右边距时,都能放进一个单行气泡;这由下面服务器的上限来保证。边距可能让空间连m₀都容不下。对于当前最长的台词,在iOS 27.0模拟器列出的所有宽度中,这包括带Duo 84点边距时直到393点的每个3×宽度和两个2×宽度,以及每侧16点时375点的2×手机。一句达到服务器上限的台词会在更多情况下放不下:日语台词在带Duo边距的402点手机上也放不下(空间286点,而m₀下需296点),拉丁字母台词在那里(302.67)、在带边距的414点3×手机上(298)以及每侧16点的360点手机上(296)也放不下:在十二种宽度乘三种边距的36种情况中,当前台词有7种,达到上限的日语台词有8种,拉丁字母台词有10种。在这些情况下,这样的台词不显示气泡,转入文本框,而较短的台词保留各自的气泡(见下文)。11 该应用只为iPhone构建,从iOS 27.0起,我Mac上随Xcode安装的iOS 27.0模拟器运行时列出了33款iPhone:3×中最窄的宽360点(iPhone 12 mini和13 mini),有三款是2×,第2代和第3代iPhone SE为375点,iPhone 11为414点。97 这是SDK所模拟的设备列表;我没有获取Apple自己关于iOS 27支持哪些iPhone的列表。气泡的空间是安全区域宽度减去32点,即限制每侧保留的16点。在竖屏手机上,没有左右边距时(假设;没有测量过任何一款),iPhone 18 Pro Max上为408点,它是本系列测量的五个视图中最窄的,但在360点的手机上为328,在375点的2×手机上为343。在Duo外屏竖屏时,没有左右边距为434,如果世界保留本系列在合上的屏幕上测得的84点尾侧边距,即侧边轨,则为350(第11节)。11979533 当前最长的台词,“Anyone have a card from the first years?”,40个字符,作为气泡在3×下的宽度为m = 4时283点,5时349点,6时416点,8时549点,即字形加上目前每侧8点的内边距;2点的描边画在气泡内部,不增加宽度。在2×下,同一句台词在m = 3时为316点,4时为416点。1125 所以气泡的m在每个视图上都有一个上限,即两种语言中最长的预设台词都能放进空间的最大值。对目前全是英语的台词而言,在18 Pro Max上(408中放349)以及列出的390到440点的每款3×iPhone上为5,在360和375点的3×手机上为4,在2×手机上为3,即2×允许的最小值;在Duo外屏竖屏时,没有左右边距为6(434中放416),有测得的侧边轨为5(350中放349.33)。一句达到下面服务器上限的日语台词,即21个全角字符、210个字体像素的步进,会把Duo的上限降到5和4,把390和393点手机的上限降到4(列出的iPhone中有七款宽390点,四款宽393点),其余不变。1197 应用根据PlazaView传入的安全宽度和服务器发送的台词计算上限,所以更窄的安全区域或更长的台词,包括日语台词,都可能降低它。上限是对所有台词的一个值;在没有上限之处怎么办,逐句决定,这是我的选择:在连m₀都放不下最长台词的地方,能以m₀放下的台词以m₀保留其气泡,放不下的台词不显示气泡,而是像大字号时那样作为单独的页面转入文本框(见下文)。若把那句台词的气泡以m₀保留,就会破坏限制:在带84点尾侧边距的360点视图上,空间为244点,而当前最长的台词在m₀下为282.67。11 服务器则把每句台词都限制在:没有左右边距时,在每种比例下最窄的手机上以最小尺寸能放下的长度:最多218个字体像素的步进,即43个拉丁字母或21个全角字符,取3×下(360 − 48) × 3 ÷ 4 = 234与2×下(375 − 48) × 2 ÷ 3 = 218中的较小者。达到这一上限的日语台词,作为3×下m = 4的气泡为296点,2×下m = 3为331点;拉丁字母台词为302.67和338.50;两者都放得进带侧边轨的Duo的350。1133 手机竖屏时的左右边距没有测量;下面那些断言气泡位于安全区域内的截图,就是它会显现之处。

最长的预设台词作为3×下PixelMplus10单行气泡的宽度横向条形图,虚线表示气泡的空间,即安全区域宽度减32,假设手机竖屏时没有左右边距:m 4时283点,在360点iPhone的空间328点之内;m 5时349,超出328但在350之内,即带84点侧边轨的iPhone Duo外屏竖屏,也在iPhone 18 Pro Max的408之内;m 6时416,超出408但在434之内,即没有左右边距的Duo外屏;m 8时549,超出全部四者。

最长的预设台词作为气泡,按尺寸,在3×下,与各视图的空间对比;由figures/make_figures.py根据measure_kiradex_text.out绘制。

更大的文字。 气泡的m跟随文本框,小一档,绝不低于m₀,也绝不高于视图的上限:在18 Pro Max上,文本框在xLarge(m = 6)时它达到5,从xxxLarge起,文本框的m为7或更大,它停在5。凡是上限把气泡压到比文本框小两档或更多的地方(18 Pro Max上从xxxLarge起,360点手机上从xLarge起),读者也会在文本框中以文本框的尺寸,作为单独的页面看到玩家六格之内说出的每句台词:说话人的昵称、一个冒号和那句台词,就像《绿宝石》在台词里写出说话人那样(第3项为它翻页;第8项播报它)。持有房间台词、如今组成文本框状态的PlazaView,组成这些页面并按第3项的队列把它们排在状态之前:立即显示,排在已在等待的任何台词页面之后,最多保留三句。它在添加某句台词气泡的同一次更新中,把该台词的页面放进文本框的队列,所以屏幕上出现这样一句台词的气泡,就意味着它的页面已经入队。它们并不总是一页。昵称为14到20个字符,所以“handle: line”为19到62个字符,而在第3项的边框下,18 Pro Max上文本框的一行在xxxLarge时容纳31个拉丁字母,AX3时17个,AX5时12个:对names.py能生成的每个昵称和每句预设台词而言,从xxxLarge到AX3为一到两页,AX4和AX5时最多三页,在360点和2×手机的最大尺寸下、以及带测得侧边轨的Duo外屏在AX4和AX5时最多四页。1189 文本框本身从不受限;它折行并翻页。dynamicTypeSize(_:)页面建议在限制尺寸时提供大内容视图;简报不依赖它,因为那个查看器自己的页面警告不要把它“as a replacement for proper Dynamic Type support”(当作正确的Dynamic Type支持的替代品)使用,而且两个页面都没有说明,它能否从一个关闭了命中测试的叠加层打开,而气泡的叠加层正是如此。707125 我的调研笔记曾提议让气泡在m = 8时停止增长,但最长的台词在8时宽549点(笔记给出553,把画在气泡内部的描边也算了进去),比18 Pro Max(440点)和Duo外屏竖屏(466)都宽,而且笔记是对照Duo外屏来衡量这个上限的,可更窄的是440点的18 Pro Max;这里的简报以宽度为准。2211

检查。 - 应用的字体测试(第1项)还断言:仓库中一张PixelMplus10的步进表,为两种语言预设台词用到的每个字符各列一项,与捆绑的字体一致。服务器的测试套件没有字体解析器(其依赖为FastAPI、uvicorn、websockets、httpx和pytest),它读取这张表,拒绝含有表中所无字符的台词,也拒绝任一语言中步进之和超过218个字体像素,即上述上限的台词。10411 - 对上限进行单元测试,上限是安全宽度、显示比例和台词步进的纯函数:在3×下以当前最长的台词,它对列出的390到440点的每个宽度给出5,对360和375给出4,对466给出6,对382给出5(Duo外屏,分别不带和带测得的侧边轨),在2×下对375和414给出3;当台词中有一句210个字体像素的日语台词时,对466给出5,对382给出4,对390和393给出4;对于3×下244点的空间(360点减去84点边距再减32),以当前最长的台词则没有上限;另有对PlazaView中组合逻辑的测试,确认在那个空间里,当前最长的台词不显示气泡,其页面进入文本框,而“Hi!”以m₀显示为气泡(宽36点,在244之内)。11 - 对BubblePlacement进行单元测试,把说话人的头放在宽360、375、440和466点视图的每条边、每个角和中心,每种视图分别在没有安全区域边距、前后两侧各16点、以及带Duo 84点尾侧边距的情况下,放上若干宽度的气泡,最大到该情况的安全宽度减32(从244点,即360减84再减32,到434),确认每个气泡都在安全区域每侧减16点的范围内,并且只要头的x位于那条带之内,气泡就跨过头的x。11 - 在iPhone 18 Pro Max、iPhone 13 mini和iPhone SE(第3代)模拟器上,以及iPhone Duo模拟器的外屏上,各截四张图(英语和日语,各在默认尺寸和AX3下),运行广场服务器(KIRADEX_PLAZA=1),应用以-worldTrace启动,每张图都由玩家说出该语言最长的台词;在每张图中,脚本找到气泡的墨色描边,并断言它是一行文字的高度,宽度不超过安全区域宽度减32点,并位于安全区域每侧减16点的范围内。脚本从生成截图的UI测试获取边距,UI测试则从一个由-worldTrace开启的隐藏元素中读取它们(第8项),PlazaView把它读到的边距写在那里(这是我的设计);脚本还根据字形的像素求出气泡的m,并断言它等于文本框的m减一(绝不低于m₀)与该安全宽度下的上限两者中的较小者,上限由构建中两种语言的台词计算,所以只有在上限起限制作用之处,它才等于上限。在默认尺寸下,无论上限如何,3×手机和Duo上为4,2×手机上为3;在AX3下它就是上限,而在Duo上由边距决定:以当前的英语台词,没有左右边距为6,有侧边轨为5,有一句达到服务器上限的日语台词时为5和4。119597 - 在iPhone 18 Pro Max模拟器的AX3下进行一项UI测试,此时文本框的m为12,一行容纳17个拉丁字母;它需要广场服务器(KIRADEX_PLAZA=1),不需要其他玩家,以-worldTrace启动,并通过第5项所说的world.textSpeed键把文字速度设为Instant(第6项),这样每一页一打开就完整显示,点按一次文本框恰好翻一页。它从“Me”标签页读取玩家的昵称,从“Say”菜单选择“Hi!”,并在现有关于广场有同伴的测试所允许的5秒内等待气泡Said: Hi!,这样这句台词已从房间返回,按第4项的规则,它的页面已进入文本框的队列,排在状态之前,无论状态多长。小镇四位收藏者每人最多每20秒说一句,他们的某句台词可能排在它前面;队列最多保留三句,且永不丢弃玩家的那一句(第3项),所以前面最多有两句,在那里每句最多两页,之后说的台词排在它后面。测试在每次点按前读取plaza-page,只在页面不是昵称、冒号、空格加“Hi!”时才点按文本框,每次间隔一秒,最多四次,只要有一次读取发现了那一页就通过,最后一次读取在第四次点按的一秒之后。它指定这句台词,是因为对names.py能生成的每个昵称,“handle: Hi!”在那里都是一页,而最长的台词可能占两页。它记录读到的每一页。11103102

5. 招牌与名字

招牌。 招牌美术保持没有文字,按kit.py的画法,这样一张图集就能服务两种语言。29 点按招牌图块时,在WorldHUD文本框中打开招牌的文字,作为排在状态之前的页面(第3项),而不是像现在这样让玩家走过去。舞台现在通过DragGesture读取触摸:touched(_:in:)让玩家立刻走向手指下方的地面,只有在收藏者或信息亭上方时例外,它们会等手指抬起,而lifted(_:in:)把这次抬起读作一次点按。招牌图块在这两处都加入收藏者和信息亭的行列:它等手指抬起,lifted(_:in:)为它调用一个onSign闭包,就像为信息亭调用onKiosk一样(名称是我的)。92 招牌文字与地图存放在一起:town.json新增signs: { "7,10": { "en": ..., "ja": ... } },以单元格为键,锻炉的检查会拒绝缺少任一语言的招牌单元格。

名字。 头顶上不显示常驻的名字标签。经典作品的对话框在台词里写出说话人,《绿宝石》有6.5%的段落如此。1 Kiradex的昵称最长20个字符;在气泡的最小尺寸下,即3×下m = 4、2×下3,一个20字符的昵称在3×下宽133点,在2×下宽150点;在3×下,它位于一个面朝镜头时宽16纹素、侧面时宽13纹素(高30,在32×40的单元格中)的人物之上,在18 Pro Max上即42.7点或34.7点。119610033 昵称像现在一样显示在点按后打开的面板里,并由VoiceOver读出(第8项)。

检查。 由于相机跟随玩家,UI测试无法知道招牌的单元格画在屏幕的哪里,所以测试以-worldTrace和-plazaTapCell 7,10启动广场,后者是由我命名的测试参数,像-plazaFacingDemo一样在PlazaStage的body中(在一个.task里)读取,它把这个单元格送进同一套处理逻辑,这套逻辑从lifted(_:in:)中提取为一个方法。在iPhone 18 Pro Max模拟器的默认尺寸下,不运行广场服务器以免有人说话,测试还传入第3项的-worldBoxSample,状态为一页的“Sample.”,并通过启动参数中的设置键把文字速度设为Instant(第6项)(这个键world.textSpeed由我拟定,与摇杆的world.stick并列)。它最多等待5秒,让plaza-page从“Sample.”变为招牌的第一页,然后读取,点按文本框,再读取,点按次数最多为招牌文字的单词数(日语则为字符数)加一,直到plaza-page读到“Sample.”为止,并确认在此之前的各页按顺序连起来,忽略空白后等于招牌的英文文字,这样长到需要翻页的招牌文字与短的一样能通过;把应用语言设为日语重新启动后,则确认日文文字。92 锻炉检查会拒绝任何含有缺少任一语言的S单元格的town.json。

6. 文字速度与翻页标记

内容。 一种按时间而非帧计数的打字机式显示:shown = floor(elapsed × cps),这是一个关于经过时间、速度以及Reduce Motion和VoiceOver是否运行这两项设置的纯函数,所以60赫兹和120赫兹的显示速度相同。设置中提供Slow,每秒12个字符,即《水晶》的慢速;Normal,30,为默认值,是我在经典作品的中档设置(《绿宝石》的15、《水晶》的20)与其最快设置60之间的选择;以及Instant。显示过程中点按会立即显示完整页,在已显示完的页面上点按则翻页。1 为此,文本框在显示过程中或其后还有页面时接受点按;在最后一页完整显示后,它像现在一直以来那样把点按透传到地面。26 两种语言速度相同,就像《最终幻想VI》两者都以每帧一个字母显示。4 开启Reduce Motion或VoiceOver正在运行时,函数立刻返回整页;这是我的选择,依据的是文档中关于Reduce Motion的那句“UI should avoid large animations”(界面应避免大幅动画)。83

标记。 一个Kiradex自有、由锻炉绘制的标记,一个5×3字体像素的向下楔形,位于文本框右下角,每150毫秒向下移动0、1、2和1个字体像素(方向是我的选择;依我的解读,《绿宝石》的是向上),一个周期600毫秒:即60赫兹下《绿宝石》每9帧一步。133

检查。 对每秒30个字符的显示函数进行单元测试:t = 0时为0个字符,1.000秒时为30个,经过时间由tick计数除以速率算出,分别以每秒60和120个模拟tick测试,而不是把每个tick的增量累加(120个1/120的增量以双精度累加得到0.9999999999999989,乘以30为29.999999999999968,向下取整为29);同一函数在开启Reduce Motion时、以及另外在VoiceOver运行时,在t = 0返回整页;在iPhone 18 Pro Max模拟器的默认尺寸和Slow下,不运行广场服务器,以-worldTrace和第3项的-worldBoxSample状态启动的一项UI测试,状态的第一页是两行,每行30到40个字符,至少60个字符,因此按每秒12个至少要显示5秒,测试确认在显示开始一秒后点按文本框,半秒之内plaza-page就变为整页,而到那时单靠显示只会显示18个字符(点按时为12个);对标记偏移随经过时间变化的函数进行单元测试,在0、150、300、450和600毫秒时为0、1、2、1和0个字体像素;以及在模拟器上对标记录屏两秒,去掉重复后,其偏移按循环顺序为0、1、2、1。33

7. 日语

内容。 先做世界文字的日语,应用其余部分以后再说。server/app/lines.py保持其索引顺序,正如其注释所说,“add at the end only”(只在末尾添加),每句台词变为一个英文字符串和一个日文字符串;服务器两者都发送,每个客户端显示其用户所读的语言,于是在一个语言混杂的广场里说出的一句话,从同一个索引被读成两种语言。28 依我的解读,这是这个应用使用预设台词的最好理由。世界的字符串,即WorldHUD的标题和状态以及任务标题,移入带有ja的String Catalog;招牌文字仍与地图放在一起(第5项)。每个日语字符串都由日语母语者撰写或审阅;没有一条未经审阅的机器翻译会被发布。

换行。 预设台词从不折行(第4项)。文本框文字按默认尺寸下每页两行来写,就像《绿宝石》的剧本是手工换行的。1 在必须折行之处,保持严格规则:除非截图显示加上标签后严格规则仍然成立,否则不给文本框文字加ja排版语言标签,因为macOS上的Core Text在加上ja标签后,在大多数字体中7个宽度里有6个(k8x12和k8x12S中7个里有4个)允许小写假名位于行首。777 如果系统默认仍然采用宽松规则,书面的修正办法是在少数需要的字符串里,在小写假名前加一个U+2060 WORD JOINER。

检查。 一项测试,确认每句台词和世界String Catalog中的每个键都具备两种语言;第4项的服务器测试,它拒绝超过218字体像素上限、即21个全角字符的日语预设台词;以及在iPhone 18 Pro Max模拟器的默认尺寸下,把设备语言设为日语,不运行广场服务器,速度为Instant,以-worldTrace启动,截取文本框中一段测试段落的每一页,该段落作为第3项的-worldBoxSample状态传入,写法使得在那里每行可容纳的22个全角字符处,换行会落在一个小写假名之前,并确认段落第一行之后,没有一行以JLREQ行首列表中的字符或其兼容形式开头。11 这份列表是完整的,即3.1.7规则所列十类的83项(有三个字符同属两类),放入测试目标中;第7节的探针用的是部分列表(第7节)。日语排版软件大多在相应位置使用Unicode为与旧标准往返而编码的兼容形式,正如JLREQ谈到前括号时所说:“the compatibility counterpart U+FF08 FULLWIDTH LEFT PARENTHESIS is used in the Japanese context in most Japanese text layout software”(在大多数日语排版软件中,日语语境下使用的是其兼容对应字符U+FF08 FULLWIDTH LEFT PARENTHESIS)。21 所以列表加入Unicode Halfwidth and Fullwidth Forms区块中,兼容规范化(NFKC)结果为这83项之一的每个字符,共24个,其中有!(U+FF01)、?(U+FF1F)、)(U+FF09)、:、;、,和.,以及半角的。、、、ー和从ァ(U+FF67)起的小写片假名;另外加入全角波浪号~(U+FF5E),它与同类的波浪线〜并列,这是我的补充,因为它的规范化结果是ASCII波浪号,而列表中没有:共25个。98 PixelMplus10映射了83项中的54项和25个形式中的24个。其余29项,包括〜(U+301C)、゠、en dash、‼和从U+31F0起的小写片假名,以及全角的⦆(U+FF60),它都不绘制,而第1项的覆盖测试会把这些全部挡在世界的字符串之外,所以截图中只可能出现它绘制的75个字符(54项中有三项,即)、〕和],同时列在两类中,因此是51个字符,再加上24个形式)。98 脚本把每行第一个字形单元格,与用CTFontGetGlyphsForCharacters从捆绑字体中以截图的m绘制的这75个字符的字形逐像素比较,确认没有一个匹配。只有在字体把它们画得不同时,像素才能把列表中的字符与段落中的其他字符区分开。在10像素下绘制时,75个中有两个在字体里有像素完全相同的孪生字符:‐(U+2010)与不换行连字符U+2011相同,冒号与修饰字母冒号U+A789相同;24个形式中没有孪生。所以测试段落两个孪生都不用;ー(U+30FC)与汉字一(U+4E00)不同,在m = 5下着墨设备像素为200对250,所以一可以出现在段落中的任何位置。9821 第7节的结果来自Mac上的Core Text;iOS上的SwiftUI怎么做,由这项截图决定。

8. 舞台上的VoiceOver

现状。 VoiceOver听到“The plaza”,接着是行走测试的记录,例如“12,4 down”。25 这个值是给测试用的,不是给人用的。

内容。 1. 舞台的标签保持“The plaza”或房间的名字,它的值变成一个人能用的句子:收藏者以地标而论站在哪里,附近有谁,他们举着什么(例如By the pond. Three collectors nearby. Holding up a card.),这是依据accessibilityValue的规则,即值说的是标签没说的东西。79 现在借stepTrace进入舞台值的一切,不论由谁写入,都要移走:我阅读PlazaStage.swift时,有来自rig.onStep的步伐记录、来自RealityView的make闭包的构建失败消息,以及来自读取-airProbe的那个.task的空气探针报告,几分钟后又多了第四处写入,来自当天添加的一个行走演示。它们移到一个只在应用以-worldTrace(名称由我拟定)启动时才存在的隐藏元素中,行走测试会传入这个参数,与文本框的页面(第3项)和PlazaView读到的边距(第4项)放在一起,而读取舞台值的那些测试,其中包括testTheCollectorFacesTheWayItWalks,以及等待value CONTAINS 'smoke 18 '的testTheChimneysSmoke,改为读取那个元素。25103 2. 在舞台上使用accessibilityDirectTouch(_:options:),让VoiceOver用户在激活后可以点按行走(iOS 17.0)。80 3. 每位收藏者的实体都获得一个AccessibilityComponent,以昵称为标签,以他们手持之物为值,让VoiceOver能够描述他们(iOS 17.0)。84 4. 在玩家六格之内说出的台词,以低优先级作为AccessibilityNotification.Announcement播报(例如Glacier Shuffle 4471: Nice card!),最多每两秒一次,即服务器自身对一句台词的速率。8291 5. 文本框打开一页时完整播报,除非它重复了第4点已经播报过的台词,第4项中大字号的页面就是这种情况。

检查。 一项UI测试,确认在没有-worldTrace时舞台的值不含坐标;在iOS 27模拟器上,用XCUIVoiceOverService开启VoiceOver、把焦点移到舞台,并断言currentSpeech()返回的内容包含舞台的标签、且不含步伐记录那种x,y形式的数字对的测试;87 在开启VoiceOver的设备上进行一项手动检查,注明日期和构建号,确认收藏者实体能够被访问并朗读,因为带虚拟相机的RealityView是否会暴露AccessibilityComponent元素尚未核实(第11节);以及对播报限流的单元测试,2秒内的两句台词只产生一次播报。

9. 关于字体与窗口,绝不做的事

  1. 应用或锻炉中不出现任何来自游戏ROM或反编译的字形、边框、箭头或窗口图像。pret和ff6的字表在这里只被测量,从不复制。Kiradex的边框、标记和招牌由锻炉绘制;它的字体是一款有授权的字体。
  2. 不使用系列角色或合字。《绿宝石》的charmap带有用于系列专有词语的特殊字形;不绘制任何类似的东西,也没有任何字符串使用该系列对事物的称呼。31
  3. 授权文件不在手的字体不用:只有授权文本已在第4或第5节中引用并与证据一起保存的字体才能发布。
  4. 修改过的OFL字体不以其保留字体名使用。Press Start 2P声明了保留字体名;如果将来修改了任何OFL字体,就给它改名。54
  5. 不把文字烘焙进世界美术:招牌、店面和公告板都保持没有文字,这样就永远不会出现按语言区分的图集。

检查。 一个脚本为锻炉写出、资源目录持有的每张图片计算两个SHA-256摘要,一个是文件字节的,一个是解码后像素的,并与这里测量过的每张PNG(pret的graphics/fonts和graphics/text_window、《水晶》的边框,以及《水晶》和《红》的gfx/font/font.png)的同样两个摘要比较。把这些摘要的列表而不是图片本身,以scripts/forge/canon_digests.txt(名称由我选定,放在锻炉的kit.py旁边)检入仓库,这样检查不需要证据文件夹也能运行,任何一个匹配都会让构建失败。它自己的测试是一个不需要任何游戏文件的阴性对照:把某个锻炉输出的摘要加入列表的副本,并断言此时脚本失败。同一个脚本还会列出供审阅、但不判为失败的条目:锻炉输出中任何一个8×8单元格,其1位掩码或其反相与那些字表的某个单元格一致。整图摘要无法重建这些单元格,所以列表中还收录了这些字表每个不同单元格掩码的SHA-256摘要,依据一条规则(我的),并以同样方式应用于锻炉的输出:颜色与图片左上角像素不同之处的像素置位;单元格从左上角起按8×8划分,边缘不完整的单元格舍去;空白或全满的单元格两边都排除,因为字表中这类单元格很多(单是japanese_normal.png的1,024个单元格中就有430个空白);掩码是逐行排列的64位,即8字节。在50张字表中,这样保留了7,746个单元格,得到1,484个不同的摘要。99 重新着色、缩放或重画的副本能通过摘要检查;规则1依靠的是锻炉从代码绘制每一项资源以及那项审阅,而不仅是这项检查。此外,本系列之前的简报所提议的字符串检查,扩展到包含《绿宝石》charmap中的系列专有缩写。105

本简报不包含的内容

  • 整个应用的像素字体。 图鉴、扫描器和设置保留系统字体;像素字体是给世界用的。
  • 中文和韩语。 所查看的四款现代像素游戏都发布了中文,其中两款,即Stardew Valley和Sea of Stars,还发布了韩语,但这里没有为其中任何一种测量像素字体。64
  • 用Misaki或任何8点阵字体排正文。 它是测量过的最小汉字;在那个尺寸下对日语读者的可读性没有测量。6
  • 头顶的名字标签和文本框中的名字栏。1
  • 没有人审阅过的机器翻译日语。
  • 像素字体上的合成粗体或斜体。
  • 出售的边框。 每种边框都免费;给支持者的边框有待决定。
  • 在RealityKit场景中绘制的文字。 世界篇的理由和第10节的算术都让它留在SwiftUI中。2311

核心要点

如果你画美术

  • 用图块搭建边框:角、边和中心。《绿宝石》的菜单从20种24×24像素、3×3图块的边框中选择;它的对话框由一条底边镜像的14图块长条绘制;《水晶》的边框各由6个1位图块组成。1
  • 给翻页标记一个缓慢的节拍。《绿宝石》的箭头从静止位置移动0、1、2和1像素,每9帧一步,一个周期0.6秒。1
  • 让世界美术里不出现文字。一块没有文字的招牌用一张图集就能服务所有语言;文字放进文本框。29
  • 日语用8到12像素绘制,并预期一个假名是一个拉丁字母的两倍:在测量的每款支持日语的像素字体中,都是8对4、10对5、12对6。7

如果你构建引擎

  • 按文件而不是按名字选择像素字体:检查网格、与基线的偏移和步进。在macOS上的Core Text中,PixelMplus12偏离基线0.39像素的网格在每个尺寸下都发虚;k8x12的8.01像素假名步进在字形被精确放置后发虚。7
  • 只在每个字体像素为整数个设备像素时绘制:在Mac上关闭字体平滑的Core Text中,网格位于基线上、步进为整数像素的字体(Misaki、PixelMplus10、Press Start 2P)在整数倍下为0.0%中间调像素,在整数倍之间为19.4%到82.2%。在上下文的默认平滑下,PixelMplus10以其本身的尺寸绘制时,272个着墨像素中有178个是中间调,所以整数倍是必要条件,不是充分条件;在iPhone上相信任何一个结果之前,先截图。79
  • 通过取整到网格来尊重Dynamic Type。在3×下,10像素的em在12档Body尺寸中只有1档为整数;把缩放后的尺寸取整为每个字体像素整数个设备像素,并把它设为固定尺寸。768
  • 以时间而不是帧来计文字速度,让60赫兹和120赫兹显示一致;每秒60个字符是测量的每款游戏中的最快速度,在有中档设置的游戏中,中档为15到20。14
  • 给VoiceOver一句话,而不是一串记录。accessibilityValue用于标签没说的东西,AccessibilityComponent描述实体,而XCUIVoiceOverService从iOS 27.0起可以测试VoiceOver朗读的内容。798487

如果你设计游戏循环

  • 一次两行,放在底部。《绿宝石》的对话框占屏幕的30%,《水晶》的占33%,都是两行。1
  • 按宽度来写。《绿宝石》的编剧在一个能容纳41个字符的窗口里,让24,100行中的95%保持在38个字符以内,没有一行超出窗口。1
  • 在台词里写出说话人,而不是放在头顶;《绿宝石》的对话框没有名字栏。1
  • 从一开始就规划日语:所查看的四款现代像素游戏四款都发布了日语。覆盖JIS X 0208的字体要画大约七千个字符;剧本定稿的游戏可以少发,就像《最终幻想VI》的日文版ROM只用了1,248个字母,但那样每个新字符串都得与字体核对。64584

常见问题

像素艺术游戏的文本框应该多大?

在掌机上的宝可梦游戏里,是屏幕下方三分之一,两行。《绿宝石》的对话边框为30×6图块,240×48像素,占Game Boy Advance屏幕高度的30%,框内是一个以16像素字体容纳两行的27×4图块文本窗口。《水晶》的对话框为20×6图块,占Game Boy屏幕的33%,两行,每行18个字符,《红》也是同样的20×6。《最终幻想VI》在屏幕顶部或底部打开它的窗口。12345

打字机式文字效果应该多快?

经典作品计的是帧。《绿宝石》和《火红》每8、4或1帧显示一个字符,在60赫兹下即每秒7.5、15或60个,在显示过程中按过一次之后,按住A或B期间每帧一个;《水晶》和《红》每秒显示12、20或60个;《最终幻想VI》以每帧一个字母、每秒60个的速度显示场景对话,且没有相应设置(其六档信息速度设定的是战斗信息等待多久)。在现代手机上,应以时间而不是帧来计字符,这样速度在120赫兹下不会改变。测量的每款游戏最快都是每秒60个,《绿宝石》和《水晶》的中档设置为15和20;下文的简报提议以30为默认值,这是我在那些中档设置与最快的60之间的选择,不是测得的设置。1144

哪些像素字体支持日语?

在测量过的字体中,Misaki(8×8点阵)、k8x12(8×12)和DotGothic16画齐了JIS X 0208全部6,879个字符,PixelMplus10和PixelMplus12画齐了除三个码位外的全部。Misaki、k8x12和PixelMplus采用一份三句话的免费授权,允许使用、复制和分发,“either commercially or noncommercially”(无论商业或非商业);DotGothic16采用SIL Open Font License 1.1。用macOS 27.0上的Core Text测量,Misaki和PixelMplus10是仅有的两款网格干净地位于基线上、步进为整数像素的字体,分别为8和10像素;PixelMplus12的网格位于基线下方0.39像素,k8x12的假名步进为8.01像素,而DotGothic16的点根本不在任何像素网格上。67171853

为什么我的像素字体在iPhone上看起来模糊?

通常是因为一个字体像素不是整数个设备像素。在macOS 27.0上关闭字体平滑的Core Text中,网格干净地位于基线上、步进为整数像素的像素TrueType字体(Misaki、PixelMplus10和Press Start 2P),在其网格的整数倍下绘制时没有中间调像素,在整数倍之间则有19.4%到82.2%的着墨像素发虚。在绘图上下文的默认平滑下,即便是整数倍也会模糊:PixelMplus10以其本身的10像素绘制,272个着墨像素中有178个是中间调。9 字体文件中的两种缺陷会在每个尺寸下造成模糊:网格偏离基线,如PixelMplus12偏离0.39像素;以及步进不是整数,如k8x12的8.01像素假名步进在字形被精确放置后那样。这项测量是在Mac上做的;在iPhone上,要由iPhone上的截图来定论。7

像素字体能支持Dynamic Type吗?

只能通过取整。在iOS的十二档Body尺寸(14到53点)中,10像素的em在3×下只在40点时落在整数设备像素上,所以用Font.custom(_:size:relativeTo:)连续缩放的像素字体,在其余十一档都偏离网格;在Mac的Core Text上,偏离网格就意味着发虚,而SwiftUI在iPhone上画出什么,要由iPhone上的截图来决定。用UIFontMetrics或ScaledMetric读出缩放后的尺寸,把它取整为每个字体像素整数个设备像素,再用Font.custom(_:fixedSize:)设置。Apple要求自定义字体实现系统字体所具备的行为,Dynamic Type即在其中,并要求应用理想情况下让人至少能把文字放大到200%。710206768

Core Text遵循日语换行规则吗?

在我于macOS 27.0上的测量中,当文字没有标注语言时,是的,而且是严格遵循:一个131字符的段落在七个宽度下换行,在Hiragino Sans和八款像素字体中,都没有一行以探针行首列表中的任何字符开头;这份列表并不完整,大部分是JLREQ的字符,其中七个是全角形式。加上ja标签后,它放宽为W3C日语排版需求所描述的较宽松规则,在大多数字体中,7个宽度里有6个出现小写假名位于行首。设备语言为日语的iPhone上SwiftUI怎么做,没有测量。721

日语像素字体需要多少个字符?

JIS X 0208是通常的覆盖目标,有6,879个:524个非汉字、2,965个第一水准汉字和3,390个第二水准汉字。《最终幻想 像素复刻版》的制作人告诉Siliconera,其日语像素字体用了“around 7,500 characters”(大约7,500个字符),而西方语言是“a few hundred”(几百个)。剧本定稿的游戏可以少发:《最终幻想VI》日文版ROM的对话字体有1,248个字母。584

本文的每个数字,都来自针对已保存来源可重新运行的脚本、2026年10月5日保存的页面,或注明日期的Kiradex源码阅读,脚注写明是哪一种;Core Text的数字来自macOS 27.0,不是iOS。第12节的简报是我的提议,并已如此标注;其中没有任何内容已经做出来。

本站相关文章:iPhone上的像素艺术世界是本系列的第一篇,包含RealityKit配方和本文所测量的文字方向;像素艺术人物:iPhone上的角色与创建器是气泡所在的那位收藏者;像素艺术建筑:iPhone上的房屋、大厅与室内有让招牌保持无字的锻炉;像素艺术的运动:iPhone上的行走、相机与门有逐字显示所对照的60和120赫兹时钟;无人在线时依然鲜活的像素小镇有预设台词及其速率;像素艺术房间和像素艺术路线是本文扩展了其检查的两份简报;致敬老宝可梦游戏而不借用它们说明了为什么经典作品只测量、从不复制;而Xcode 27.1 Beta:在iPhone Duo模拟器中运行你的应用介绍了气泡截图所运行的模拟器。

参考来源


  1. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_gen_text.py,输出为measure_gen_text.out。输入来自pret pokeemerald的731ad5b:src/text.c(sFontInfos、RENDER_STATE_HANDLE_CHAR分支、sWindowVerticalScrollSpeeds、sDownArrowYCoords、日语字形函数)、src/menu.c(sTextSpeedFrameDelays、sStandardTextBox_WindowTemplates、针对标准窗口求值的WindowFunc_DrawDialogueFrame)、src/fonts.c和charmap.txt(字形宽度)、graphics/text_window/*.png(尺寸)、include/text_window.h、data/text/*.inc和data/maps/*/scripts.inc的每一行(在\n、\l和\p处切分并通过宽度表测量;504个文件,不含占位符的24,100行,含占位符的3,000行,跳过0行),以及graphics/fonts/japanese_*.png(非空单元格)。来自pokefirered的037335f:src/new_menu_helpers.c。来自pokecrystal的5beda23:constants/hardware.inc、constants/text_constants.asm、home/print_text.asm、gfx/font.asm、边框PNG和gfx/font/font.png。来自pokered的d2704a6:data/text_boxes.asm、constants/ram_constants.asm、gfx/font/font.png。每秒字符数为60除以每字符帧数。对《绿宝石》打印器的解读(延迟由AddTextPrinter减一后存储,然后倒数,所以一个字符花费的帧数等于延迟)、src/battle_message.c和src/scrcmd.c中自动滚动延迟的条件,以及对《水晶》常量的两行解读,都由脚本断言,没有运行。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  2. pret,pokeemerald/src/menu.c,提交731ad5b(第77行的sTextSpeedFrameDelays;第84行的sStandardTextBox_WindowTemplates;第212和213行的LoadMessageBoxGfx和LoadUserWindowBorderGfx;第218行调用的WindowFunc_DrawDialogueFrame,及其从第378行起使用BG_TILE_V_FLIP的填充),2026年10月5日查阅,https://github.com/pret/pokeemerald/blob/master/src/menu.c ↩↩↩↩↩↩↩

  3. pret,pokecrystal/constants/text_constants.asm,提交5beda23(第24到32行:BORDER_WIDTH EQU 2、TEXTBOX_WIDTH EQU SCREEN_WIDTH、TEXTBOX_HEIGHT EQU 6、TEXTBOX_Y EQU SCREEN_HEIGHT - TEXTBOX_HEIGHT、TEXTBOX_INNERY EQU TEXTBOX_Y + 2)和constants/hardware.inc(第922和923行,SCREEN_WIDTH 20和SCREEN_HEIGHT 18),2026年10月5日查阅,https://github.com/pret/pokecrystal/blob/master/constants/text_constants.asm ↩↩↩↩

  4. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_ff6.py,输出为measure_ff6.out。输入来自everything8215/ff6的8130132:tools/rip_list_en.json和tools/rip_list_jp.json(窗口图像、调色板和字体的字节范围)、src/menu/config.asm(ConfigWindowNumPos、LoadWindowPal、ConfigColorBarTextList、ChangeConfigOption_02)、src/menu/menu_sram.asm、src/btlgfx/gfx_cmd.asm(MsgSpeedTbl、mess_wait)、src/下每个读取$1d4d的文件、src/field/text.asm(LoadLetterGfx、行的x边界、CalcTextWidth、NewLine、UpdateDlgTextOneLine、地图标题)、src/gfx/font_gfx.asm(FontWidth)、src/field/reset.asm(场景循环)、src/field/hdma.asm(DlgWindowFixedColorTbl)以及tools/char_table/(kanji.json、kana.json)。每帧一个字母,以及渐变每条扫描线一个值,都是对代码的解读,只作断言,没有运行。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  5. Unicode联盟,JIS0208.TXT,JIS X 0208到Unicode的映射表,2.0版,归于OBSOLETE/EASTASIA,2026年10月5日获取并保存,https://www.unicode.org/Public/MAPPINGS/OBSOLETE/EASTASIA/JIS/JIS0208.TXT 。由measure_coretext.swift和measure_bdf.py统计:6,879个字符;524个非汉字,其中169个为第4和第5区的假名;第16到47区的2,965个第一水准汉字;第48到84区的3,390个第二水准汉字。 ↩↩↩↩↩↩↩↩

  6. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_bdf.py,输出为measure_bdf.out。输入:Misaki Gothic、Misaki Gothic 2nd和Misaki Mincho以及k8x12、k8x12L和k8x12S的BDF文件(2021-05-05版),以及保存的JIS0208.TXT;它报告每款字体的像素尺寸、边界框、上伸部和下伸部、字符数、步进宽度、着墨在某一方向上填满单元格的全角字形、汉字(U+4E00到U+9FFF)的相应情况,以及所画出的JIS X 0208字符。 ↩↩↩↩↩↩↩↩↩↩↩

  7. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_coretext.swift,用swiftc -O编译并在macOS 27.0(版本26A428)上运行,输出为measure_coretext.out,不是在iOS上。输入:Misaki Gothic、Misaki Gothic 2nd、k8x12和k8x12S(2021-05-05的TTF版本),PixelMplus10 Regular和Bold以及PixelMplus12 Regular(PixelMplus的d89b95f),DotGothic16、Press Start 2P和Silkscreen(google/fonts的9710da1),作为系统对照的Hiragino Sans W3,以及保存的JIS0208.TXT。它报告每款字体的字形数和位图表(CTFontCopyAvailableTables);其设计网格(4到40之间,使样本字形95%的轮廓点落在0.04像素以内的最小每em像素数,每个轴允许一个固定偏移);其拉丁字母和假名步进;其对ASCII和JIS X 0208的覆盖;把一行样本以网格1到4倍绘制到8位灰度位图时着墨像素中中间调(255中的8到247)所占比例,分别在开启抗锯齿、关闭抗锯齿、以及开启抗锯齿并开启子像素定位、关闭量化的情况下,不允许字体平滑;对PixelMplus12,还有把基线按偏移量移动后的同样测量;Human Interface Guidelines中在3×下字体像素为整数的Body和Caption 1尺寸;以及用CTFramesetter在8到20 em下排版的一段131字符日语段落每行的首字符,分别不加标签和加上kCTLanguageAttributeName “ja”,与一份行首禁则字符列表对照。DotGothic16没有设计网格,所以按名义上的每em 16点(每点62.5单位)及其1.25到4倍绘制,并以每em 32像素排版,这一尺寸是根据它的名字和measure_outline_coords.out中的点距选定的,输出会打印出来。每次渲染都不允许字体平滑(setAllowsFontSmoothing(false))。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  8. Jenni Lada,“Interview: Preparing Final Fantasy Pixel Remaster for the Switch and PS4”,Siliconera,2023年6月2日,制作人Naofumi Takuma谈新的字体选项(“We didn’t have any alternative data saved on file in-house for the Japanese language font, so we decided to create a new one”;“around 7,500 characters”对“a few hundred”),2026年10月5日获取,https://www.siliconera.com/interview-preparing-final-fantasy-pixel-remaster-for-the-switch-and-ps4/ ↩↩↩↩↩↩

  9. 作者测量,2026年10月5日:本文草稿文件夹中的figures/dump_glyphs.swift,用swiftc -O编译并在macOS 27.0(26A428)上运行,输出为figures/glyphs.txt:由Core Text以10像素绘制的PixelMplus10 Regular,“Nice card!”在不允许字体平滑时(与measure_coretext.swift的绘制方式相同)94个着墨像素中0个为中间调,在位图上下文默认设置下272个中178个;日语样本行为164个中0个和451个中287个。 ↩↩↩↩↩↩↩↩↩↩

  10. Apple,Human Interface Guidelines,“Typography”(iOS和iPadOS的默认和最小文字尺寸,17点和11点;“Follow the recommended default and minimum text sizes for each platform”;“If you use a custom font, make sure it implements the same behaviors”;iOS的Dynamic Type表),2026年10月5日获取,https://developer.apple.com/design/human-interface-guidelines/typography ↩↩↩↩↩↩↩

  11. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_kiradex_text.py,输出为measure_kiradex_text.out,于太平洋时间12:34针对提交334fbfe加上当天未提交修改的Kiradex运行,那是当天正在修改的工作树;脚本按符号而不是按行整份读取文件。输入只从当时磁盘上的Kiradex仓库读取:server/app/lines.py(23句,3到40个字符,中位数16),server/app/names.py(由各20个词的列表中取两个词加四位数字组成的昵称,最多20个字符),Kiradex/World/*.swift中每一处.kit941Font调用(四个文件中caption 10、label 3、body 3、title 3),主题的字体家族,气泡的修饰符,舞台的辅助功能修饰符,在应用源码中搜索AccessibilityComponent、accessibilityDirectTouch、updatesFrequently、AccessibilityNotification、typesettingLanguage、UIFontMetrics、ScaledMetric和dynamicTypeSize(均为0),knownRegions(Base、en),以及.xcstrings和.lproj文件的数量(0);按PlazaRig.fit的规则,针对本系列从Xcode 27模拟器配置中测得的设备尺寸(iPhone 18 Pro Max 440×956点,iPhone Duo外屏466×678,内屏669×951,均为3×)计算的世界缩放;以及从measure_coretext.out取每款候选字体的em和步进,算出在48点边距之后文本框每行的字符数,最长预设台词在m = 4、5、6和8时的气泡宽度(283、349、416和549点:该尺寸下的步进加上气泡每侧8点的内边距,描边画在气泡内部),以及10像素em下取整到m的Body尺寸,半数进一。10月5日稍后增补,早先的脚本和输出保留在旁:2×下的取整,使用不小于11点的最小m(3×为4,2×为3);简报边框下的文本框(宽度减32点后向下取整到整数字体像素,每侧8个字体像素的边框;18 Pro Max上m = 5时由1,224个设备像素变为1,220,每行45个拉丁字母或22个全角字符);names.py全部400个词对与全部23句台词在十二档Body尺寸、五个视图下“handle: line”的页数(每页两行,在空格处贪心换行,按probe_line_start_glyphs.out,每个可打印ASCII字符宽5个字体像素);measure_ios27_iphones.out中每款iPhone上气泡的上限;服务器218字体像素的上限;以及120次累加1/120的和。当天更晚些时候增补,早先的脚本和输出保留在旁,输出中原有各行均未改变:安全宽度上的空间和上限,手机为没有左右边距,Duo外屏竖屏则分别为没有边距和带本系列Duo文章测得的84点尾侧边距,后者从那篇文章中解析得出(安全宽度382,空间350,349.33点时上限为5;默认尺寸下边框内的文本框为38个拉丁字母或19个全角字符,按目前的边距为40或20个);该安全宽度下“handle: line”的页数;放置测试所用的宽度;该空间中服务器上限的情况(m = 4时296和302.67点,在350之内);以及从server/app/npcs.py中得出的小镇四位收藏者、他们的昵称、18 Pro Max在AX3下他们台词的页数(最多2页),以及第4项测试的点按上限,(3 − 1)句 × 2页,在Instant下每页点按一次:4。10月15日,针对测试现在的样子重新计算了最后这一行,就输出所示,使用相同的输入(舞台文件取12:28读取的10月5日副本;lines.py、names.py和npcs.py自10月2日和3日以来未变;其他输入读自提交fc9d4876的当前代码树,打印出相同的行),早先的脚本和输出保留在旁;输出的其他各行打印如前。10月15日稍后增补,早先的脚本和输出保留在旁,原有各行均未改变:放置测试中当前最长台词在任何允许的m下都放不下的情况(3×下带84点边距的360和375点、2×下的375,以及每侧16点的2×下375:第一种情况中,244点的空间对m₀下的282.67点);有一句达到服务器上限的日语台词,即21 × 10 = 210个字体像素时的上限(18 Pro Max上为5,Duo上不带和带侧边轨分别为5和4,3×下360点为4,2×下为3);以及截图应当找到的气泡m,即文本框的m减一(不低于m₀)与两种语言上限两者中的较小者(3×视图默认尺寸下为4,2×下为3;AX3下为上限)。当晚同样增补:measure_ios27_iphones.out列出的每个宽度,分别在没有左右边距、每侧16点和84点尾侧边距下,与当前的英语上限、带一句达到上限的日语台词时的上限以及两者合并的上限(无边距:360和375为4,带日语台词时390和393为4,402到440为5,2×下为3);当前最长台词在任何允许的m下都放不下的情况(带84点边距时3×下的360、375、390和393点,以及2×下的375和414;每侧16点时2×下的375);以及m₀下的“Hi!”,3×下36点,2×下38.50点,在每种情况下都放得下。此后增补:同一张表加入一句达到上限的拉丁字母台词,43 × 5 = 215个字体像素,与当前台词和日语台词并列,以及各自在任何允许的m下都放不下的情况:36种中分别为7、8和10种。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  12. pret,pokered/data/text_boxes.asm,提交d2704a6,第10行(db MESSAGE_BOX, 0, 12, 19, 17),2026年10月5日查阅,https://github.com/pret/pokered/blob/master/data/text_boxes.asm ↩↩

  13. pret,pokeemerald/src/text.c,提交731ad5b(从第119行起的sFontInfos;第75行的sDownArrowYCoords { 0, 1, 2, 1 }和第76行的sWindowVerticalScrollSpeeds;在第281行设置textSpeed = speed、在第296行执行--sTempTextPrinter.textSpeed;的AddTextPrinter;第832行箭头的downArrowDelay = 8;在RENDER_STATE_HANDLE_CHAR分支中,第944行的if (JOY_HELD(A_BUTTON | B_BUTTON) && subStruct->hasPrintBeenSpedUp),第947行的倒数if (textPrinter->delayCounter && textPrinter->textSpeed),第950到953行的canABSpeedUpPrint以及新按下A或B时设置hasPrintBeenSpedUp,以及第958到961行把计数器设为3、否则设为textSpeed的if (!(gBattleTypeFlags & BATTLE_TYPE_RECORDED) && gTextFlags.autoScroll);第1094行的EXT_CTRL_CODE_JPN;从第1853行起的DecompressGlyph_Normal和从第1887行起的GetGlyphWidth_Normal),2026年10月5日查阅,https://github.com/pret/pokeemerald/blob/master/src/text.c ↩↩↩↩↩↩↩↩↩↩↩

  14. pret,pokefirered/src/text.c,提交037335f,第639到647行(在RENDER_STATE_HANDLE_CHAR分支中,把计数器设为0的if (JOY_HELD(A_BUTTON | B_BUTTON) && subStruct->hasPrintBeenSpedUp),以及在延迟倒数期间设置hasPrintBeenSpedUp的gTextFlags.canABSpeedUpPrint && JOY_NEW(A_BUTTON | B_BUTTON)),2026年10月5日查阅,https://github.com/pret/pokefirered/blob/master/src/text.c ↩↩↩

  15. pret,pokecrystal/home/print_text.asm,提交5beda23(第4行,“The text speed setting in wOptions is actually a frame count”,以及按住A或B时每个字母等待一帧的PrintLetterDelay)和pokered/constants/ram_constants.asm的d2704a6(第38到40行:TEXT_DELAY_FAST 1、TEXT_DELAY_MEDIUM 3、TEXT_DELAY_SLOW 5),2026年10月5日查阅,https://github.com/pret/pokecrystal/blob/master/home/print_text.asm ↩↩

  16. pret,pokecrystal/home/joypad.asm,提交5beda23(第342行的WaitPressAorB_BlinkCursor;第442行的BlinkCursor,把hlcoord 18, 17处的图块在'▼'与'─'之间切换),2026年10月5日查阅,https://github.com/pret/pokecrystal/blob/master/home/joypad.asm 。闪烁周期没有推导。 ↩↩↩

  17. Num Kadoma,littlelimit.net,“font”,Misaki、k8x12及作者其他字体的授权部分(“These fonts are free software. Unlimited permission is granted to use, copy, and distribute them, with or without modification, either commercially or noncommercially.”),2026年10月5日获取,https://littlelimit.net/font.htm#license ↩↩↩↩↩

  18. itouhiro,PixelMplus,README.md(仅含轮廓的TrueType,“埋め込みビットマップは なし”;“JIS第1・第2水準のすべての漢字を収録”以及ISO-8859-1;PixelMplus12和PixelMplus10;由M+ BITMAP FONTS制作;授权为M+ FONT LICENSE)和misc/mplus_bitmap_fonts/LICENSE_E(M+ FONT LICENSE的三句话),提交d89b95f,2026年10月5日查阅,https://github.com/itouhiro/PixelMplus/blob/master/misc/mplus_bitmap_fonts/LICENSE_E ↩↩↩↩↩

  19. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_outline_coords.swift,在macOS 27.0上编译运行,输出为measure_outline_coords.out:PixelMplus12 Regular和DotGothic16中H、A、o和あ,以及Silkscreen中H、A和o的原始x和y轮廓坐标,以每em 1,000单位计。 ↩↩↩↩↩↩↩

  20. Apple,Human Interface Guidelines,“Accessibility”(“Ideally, give people the option to enlarge text by at least 200 percent (or 140 percent in watchOS apps).”),2026年10月5日获取,https://developer.apple.com/design/human-interface-guidelines/accessibility ↩↩↩

  21. W3C,“Requirements for Japanese Text Layout”,工作组说明,2020年8月11日,第3.1.7节(行首禁则规则及关于较宽松规则的说明),2026年10月5日获取,https://www.w3.org/TR/jlreq/ ↩↩↩↩↩↩↩↩

  22. 作者为本文所作的调研资料,2026年10月5日(未公开):上述测量、案例研究,以及为Kiradex所写简报的初稿。 ↩↩↩

  23. Blake Crosley,“Pixel-Art Worlds on iPhone: What the 16-Bit Masters Knew”,blakecrosley.com,2026年10月3日:规则10(“Text is art”、m5x7页面的“use font size 16, 32, 48, etc”,以及关于《绿宝石》24×24文本窗口边框的那句话),关于k = 8和k = 6时文字尺寸的算术,引用GoNintendo称约7,500个日语字符的像素复刻版段落,以及文字计划(“m5x7 at integer multiples in a 24-by-24 nine-slice box, two lines, typewriter reveal, a two-frame advance arrow”),2026年10月5日查阅,https://blakecrosley.com/blog/pixel-art-world-on-iphone 。那篇文章在此被引用,未作改动。 ↩↩↩↩↩↩↩↩↩↩

  24. Kiradex源码,Kiradex/KiradexTheme.swift,第12行(let fontFamily: Kit941.FontFamily = .systemDesign(.monospaced)),于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库)。 ↩↩

  25. Kiradex源码,Kiradex/World/PlazaStage.swift,于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库):第146、155和189行(stepTrace在RealityView的make闭包中被设为构建失败信息,在rig.onStep中被设为每一步,在-airProbe下于一个.task中被设为空气报告;12:42再读同一文件时出现了第四处,即新的-plazaLapDemo下的stepTrace = "lap done"),第58到61行(stepTrace,“The player’s last step, “x,y facing”, as the stage’s accessibility value: the walks read it, since nothing else the simulator logs reaches them.”),第313到315行(.accessibilityElement()、.accessibilityLabel("The plaza")、.accessibilityValue(stepTrace)),第289到293行(气泡,即第121到294行舞台GeometryReader内的最后一个.overlay),第295到309行(舞台覆盖在其上的两个叠加层:扫描线,每3点一条1点高的.black.opacity(0.16),以及RadialGradient暗角),第402到418行(bubbles(of:in:):在timeline.date.timeIntervalSince(said.at) < 4期间显示一句台词,放在头部视图点减去Kit941.Spacing.lg处,没有限制,关闭命中测试),以及第444到459行(Bubble视图,.padding(.horizontal, Kit941.Spacing.sm)之后是.overlay(Rectangle().strokeBorder(...)),其注释“A line said, as the old games drew it: a cream box with a dark border.”,caption粗体,WorldInk.paper,Kit941.Spacing.xxs的墨色描边,.fixedSize(),.accessibilityLabel("Said: \(text)"))。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  26. Kiradex源码,Kiradex/World/WorldHUD.swift,于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库);该文件日期为10月4日,读取期间没有变化:第31到38行(WorldInk,“The world’s own colours, as the old games drew a text box: cream paper”,及其纸色),第40到44行(地点的那行文字“in a text box at the bottom. Half open, the box keeps to the wider side of the fold.”),第61到79行(GeometryReader中的界面元素,用.padding(Kit941.Spacing.md)内缩,文本框横跨该宽度,半开时为较宽一侧窗格的宽度每侧减去Kit941.Spacing.md),以及第84到101行(文本框:标题为caption粗体、大写;状态为caption半粗体,标识符<name>-status,在广场中为plaza-status;纸色背景;Kit941.Spacing.xxs的墨色描边;注释“Taps go through to the ground under it.”之下的.allowsHitTesting(false));以及Kiradex/World/PlazaStage.swift第581和582行,PlazaView在那里把WorldHUD覆盖在整个舞台之上,以words作为其状态,和第725到736行(words:一天中时段的短语、在场人数和一句提示、获得的绶带以及季节的短语)。套件的间距,即941Kit中的Sources/Kit941/Tokens/Spacing.swift:在iPhone上xxs 2、xs 4、sm 8、md 16、lg 24点。 ↩↩↩↩↩↩↩

  27. Kiradex源码,Kiradex.xcodeproj/project.pbxproj,第1138到1141行(knownRegions = (Base, en)),于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库);到12:42该文件行数已增加。 ↩↩

  28. Kiradex源码,server/app/lines.py,于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库):文档字符串“The preset lines: everything anyone can say in the plaza.”,LINES的23个条目(从第5行起),以及第20行、索引14之前的注释“Indices are fixed; add at the end only, the app shows them by number.” ↩↩↩↩

  29. Kiradex源码,于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库):scripts/forge/kit.py,第529和530行(def sign():,“A hanging sign on a bracket, 12 × 11.”),Kiradex/World/TileMap.swift,第52行(地图字母"S": .sign),以及Kiradex/Resources/World/town.json(一个S单元格,位于x 7、第10行,12:33用Python的json模块读取)。 ↩↩↩↩

  30. 方法。pret反编译,读取的浅克隆为pokeemerald 731ad5b(2026年10月1日)、pokefirered 037335f(2026年9月26日)、pokecrystal 5beda23(2026年9月29日)和pokered d2704a6(2026年9月22日),https://github.com/pret 。《最终幻想VI》来自everything8215/ff6反汇编的813013276c952fdd27edcf7b8b86f17291542cbf(2026年9月14日),它收录的是代码和提取列表,不是ROM的图像,https://github.com/everything8215/ff6 。PixelMplus在d89b95f04eeb95bf98bb09cb9234a2f99d186df5(2013年6月2日)处浅克隆;Misaki和k8x12为2021-05-05版的TTF和BDF,从作者网站下载;Press Start 2P、Silkscreen和DotGothic16(字体文件、OFL.txt和METADATA.pb)来自google/fonts的9710da1(2026年9月30日)。网页于2026年10月5日获取并保存。本文作者证据文件夹中的十三个脚本,其中十个为Python、三个为Swift,再加上与本文配图放在一起的另一个Swift脚本dump_glyphs.swift,产生了每一个测得的数字,每个脚本的输出都保存在旁边。pret构建的是英文版游戏;没有读取任何日版ROM。 ↩↩↩↩↩↩

  31. pret,pokeemerald/src/fonts.c(从第148行起的gFontNormalLatinGlyphWidths)和pokeemerald/charmap.txt(第50到52行,用于系列专有词语的特殊字形),提交731ad5b,2026年10月5日查阅,https://github.com/pret/pokeemerald/blob/master/src/fonts.c ↩↩

  32. Martin Korth,GBATEK,“LCD Dimensions and Timings”(“Total 228 lines, 16.743 ms, 280896 cycles - ca. 59.737 Hz”;160条可见行;每条可见行240点),2026年10月5日获取,https://problemkaputt.de/gbatek-lcd-dimensions-and-timings.htm ↩

  33. 作者基于测得数字的算术:216 ÷ 5.25 = 41个字符;59.737 ÷ 1 = 每秒59.7个字符;191 × 1.25² = 298像素;PixelMplus12的−33单位 ÷ 每像素83.33单位 = −0.396像素(探针报告−0.39,即其所在区间的中心);7,500对6,879;按1和3个整数尺寸的补数,12档Body尺寸中有11档和9档偏离网格;14 ÷ 17 = 82%,53 ÷ 17 = 312%;13.33 ÷ 16.67 = 80%,53.33 ÷ 16.67 = 320%;11点 ÷ 每纹素2点 = 5.5纹素;4 × 10 ÷ 3 = 13.33点;12个字体像素 × 5 ÷ 3 = 20点;440 − 32 = 408、360 − 32 = 328、375 − 32 = 343点;2×下40 × 5 × 3 ÷ 2 + 16 = 316点;⌈11 × 3 ÷ 10⌉ = 4、⌈11 × 2 ÷ 10⌉ = 3,10 × 3 ÷ 2 = 15点;(360 − 48) × 3 ÷ 4 = 234和(375 − 48) × 2 ÷ 3 = 218个字体像素,218 ÷ 5 = 43个拉丁字母,218 ÷ 10 = 21个全角字符;21 × 10 × 4 ÷ 3 + 16 = 296和21 × 10 × 3 ÷ 2 + 16 = 331点;(440 − 32) × 3 = 1,224个设备像素,÷ 5 = 244.8,故为244个字体像素(1,220个设备像素),减去2 × 8得228,÷ 5 = 45个拉丁字母,÷ 10 = 22个全角字符;15 × 3 ÷ 10 = 4.5,进为5;以双精度累加120次1/120 = 0.9999999999999989,× 30 = 29.999999999999968,向下取整为29;1.5秒 × 12 = 18个字符;20 × 5 × 4 ÷ 3 = 133点,2×下20 × 5 × 3 ÷ 2 = 150点;466 − 84 = 382、382 − 32 = 350点,466 − 32 = 434;40 × 5 × 5 ÷ 3 + 16 = 349.33点;(382 − 32) × 3 ÷ 5 = 210个字体像素,减去2 × 8得194,÷ 5 = 38个拉丁字母,÷ 10 = 19个全角字符;(382 − 48) ÷ (5 × 5 ÷ 3) = 40.08,÷ (10 × 5 ÷ 3) = 20.04;43 × 5 × 4 ÷ 3 + 16 = 302.67点;360 − 84 − 32 = 244点;(3 − 1) × 2 × 1 = 4次点按;16纹素 × 2.67点 = 42.7点,13 × 2.67 = 34.7;9帧 ÷ 60 = 150毫秒,四步600毫秒;3点 × 3 = 扫描线间隔9个设备像素,每条1点 × 3 = 3个设备像素高。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  34. pret,pokeemerald/src/battle_message.c,提交731ad5b(第3002和3003行:当gBattleTypeFlags & (BATTLE_TYPE_LINK | BATTLE_TYPE_RECORDED)时开启自动滚动;第3010行,联机对战消息窗口的speed = 1;第3012行,录像对战的speed = sRecordedBattleTextSpeeds[GetTextSpeedInRecordedBattle()],第1957行的表{8, 4, 1, 0})和pokeemerald/src/scrcmd.c(从第1286行起的ScrCmd_messageautoscroll,在第1292和1293行设置autoScroll和forceMidTextSpeed),2026年10月5日查阅,https://github.com/pret/pokeemerald/blob/master/src/battle_message.c ↩↩↩

  35. pret,pokeemerald/charmap.txt,提交731ad5b,第1087到1089行('\l' = FA @ scroll up window text、'\p' = FB @ new paragraph、'\n' = FE @ new line),以及include/constants/characters.h,第175和176行(CHAR_PROMPT_SCROLL,“waits for button press and scrolls dialog”;CHAR_PROMPT_CLEAR,“waits for button press and clears dialog”),并结合src/text.c第968到971行(CHAR_NEWLINE把currentY下移一行)和第1102到1109行(两种提示进入RENDER_STATE_CLEAR和RENDER_STATE_SCROLL_START),2026年10月5日查阅,https://github.com/pret/pokeemerald/blob/master/charmap.txt ↩

  36. pret,pokeemerald/src/text_window.c(第53行的message_box.png;第93行的LoadMessageBoxGfx;第110行的LoadUserWindowBorderGfx),pokeemerald/include/text_window.h(第4行的#define WINDOW_FRAMES_COUNT 20),pokeemerald/src/option_menu.c(FRAME选项)和pokeemerald/graphics/text_window/,提交731ad5b,2026年10月5日查阅,https://github.com/pret/pokeemerald/blob/master/src/text_window.c ↩↩↩

  37. pret,pokeemerald/data/text/和每张地图的scripts.inc,提交731ad5b,其中包括data/maps/LittlerootTown/scripts.inc第854行(.string "MOM: {PLAYER}, we're here, honey!\p"),2026年10月5日查阅,https://github.com/pret/pokeemerald/blob/master/data/maps/LittlerootTown/scripts.inc ↩↩

  38. pret,pokeemerald/src/international_string_util.c、src/contest.c和src/battle_tower.c(名字中的EXT_CTRL_CODE_JPN和EXT_CTRL_CODE_ENG代码)以及graphics/fonts/(japanese_normal.png、japanese_small.png、japanese_bold.png、japanese_frlg_male.png),提交731ad5b,2026年10月5日查阅,https://github.com/pret/pokeemerald/blob/master/src/international_string_util.c ↩↩

  39. pret,pokefirered/src/new_menu_helpers.c,提交037335f(从第34行起的sStandardTextBox_WindowTemplates,第40行的.width = 26,以及文字速度延迟)和pokefirered/src/text_printer.c(AddTextPrinter,第78行的textSpeed = speed和第93行的--sTempTextPrinter.textSpeed;),提交037335f,2026年10月5日查阅,https://github.com/pret/pokefirered/blob/master/src/new_menu_helpers.c ↩

  40. Pan Docs,“Rendering”(“One frame: 70224 dots @ 59.7 fps”;一帧“takes ~16.74 ms instead of ~16.67”),gbdev.io,2026年10月5日获取,https://gbdev.io/pandocs/Rendering.html ↩

  41. pret,pokecrystal/gfx/font.asm,提交5beda23(边框FRAME_1到FRAME_8,TEXTBOX_FRAME_TILES每种6个1位图块,一个未使用的第九种边框)和gfx/font/font.png,2026年10月5日查阅,https://github.com/pret/pokecrystal/blob/master/gfx/font.asm ↩

  42. everything8215,ff6,tools/rip_list_en.json和tools/rip_list_jp.json,提交8130132(位于0xED0000到0xED1BFF的assets/gfx/window/window_%s.4bpp;位于0xED1C00到0xED1CFF的window_%s.pal),2026年10月5日查阅,https://github.com/everything8215/ff6/blob/master/tools/rip_list_en.json ↩

  43. everything8215,ff6,src/menu/config.asm(ConfigWindowNumPos、从$1d57读取的LoadWindowPal、位于$1d55的字体颜色、ConfigColorBarTextList、ChangeConfigOption_02)和src/menu/menu_sram.asm(第102行,“message speed = 3, battle speed = 3”),提交8130132,2026年10月5日查阅,https://github.com/everything8215/ff6/blob/master/src/menu/config.asm ↩↩↩

  44. everything8215,ff6,提交8130132:src/btlgfx/gfx_cmd.asm第1101到1103行(MsgSpeedTbl,“wait duration for each battle message speed”,$20,$30,$40,$50,$60,$70,$80,$90)和第1151到1165行(mess_wait,把$1d4d的第4到6位读作索引,并调用相应次数的WaitFrame);src/menu/config.asm第1499行(“battle speed, battle message speed, magic order, and window pattern”)和从第933行起的ChangeConfigOption_02;src/menu/menu_text.en.inc第346行(设置以1到6绘制)。代码涉及$1d4d的文件:src/battle/init.asm(指令设置、战斗模式和战斗速度)、src/btlgfx/gfx_cmd.asm、src/menu/config.asm、src/menu/field_menu.asm和src/menu/menu_sram.asm;src/field/下一个也没有。2026年10月5日查阅,https://github.com/everything8215/ff6/blob/master/src/btlgfx/gfx_cmd.asm ↩

  45. everything8215,ff6,src/field/hdma.asm(DlgWindowFixedColorTbl)和src/field/event.asm(第1591行,“dialog window y position (1 or 18)”),提交8130132,2026年10月5日查阅,https://github.com/everything8215/ff6/blob/master/src/field/hdma.asm ↩↩↩

  46. everything8215,ff6,src/field/text.asm(LoadLetterGfx、从x 4到$e0的对话行、CalcTextWidth、NewLine、UpdateDlgTextOneLine、居中显示100帧的地图标题)和src/gfx/font_gfx.asm(FontWidth),提交8130132,2026年10月5日查阅,https://github.com/everything8215/ff6/blob/master/src/field/text.asm ↩↩↩↩↩

  47. everything8215,ff6,tools/rip_list_jp.json(位于0xC494C0到0xC4FFFF的large_font.jp.1bpp)和tools/char_table/,提交8130132,2026年10月5日查阅,https://github.com/everything8215/ff6/tree/master/tools/char_table ↩

  48. everything8215,ff6,src/field/reset.asm(场景循环:等待垂直消隐,每轮调用一次UpdateDlgText),提交8130132,2026年10月5日查阅,https://github.com/everything8215/ff6/blob/master/src/field/reset.asm 。该反汇编从同一份源码构建英文版和日文版两个ROM。 ↩↩

  49. Data Crystal,“Chrono Trigger/ROM map”,datacrystal.tcrf.net,2026年10月5日获取:HTTP 403,未能读取,https://datacrystal.tcrf.net/wiki/Chrono_Trigger/ROM_map ↩↩↩

  50. Kiradex,docs/TestFlight.md,第5步(第192到194行:在图鉴中选择日语时,系列和卡牌以日文名显示,下方附英文),于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库)。 ↩

  51. Num Kadoma,littlelimit.net,“8×8ドット日本語フォント「美咲フォント」”(概述中的8×8单元格、JIS第一和第二水准以及7×7的着墨;Misaki Mincho和Misaki Gothic 2nd的说明;M+ FONTS授权的说明;为PC-E500日语显示驱动而制作的经过),2026年10月5日获取,https://littlelimit.net/misaki.htm ↩↩↩↩

  52. Num Kadoma,littlelimit.net,“8×12ドット日本語フォント「k8x12」”(8×12单元格,JIS第一和第二水准,L和S两个变体,S变体关于旧游戏机8×8图形的说明),2026年10月5日获取,https://littlelimit.net/k8x12.htm ↩

  53. Google Fonts,ofl/dotgothic16/OFL.txt(SIL Open Font License 1.1,Copyright 2020 The DotGothic16 Project Authors,无保留字体名)和METADATA.pb(设计者Fontworks Inc.;子集cyrillic、japanese、latin、latin-ext;2020-12-15加入),9710da1,2026年10月5日查阅,https://github.com/google/fonts/blob/main/ofl/dotgothic16/OFL.txt ↩↩↩↩

  54. Google Fonts,ofl/pressstart2p/OFL.txt(SIL Open Font License 1.1,“with Reserved Font Name “Press Start 2P””)和METADATA.pb(设计者CodeMan38),9710da1,2026年10月5日查阅,https://github.com/google/fonts/blob/main/ofl/pressstart2p/OFL.txt ↩↩↩

  55. Google Fonts,ofl/silkscreen/OFL.txt(SIL Open Font License 1.1,无保留字体名)和METADATA.pb(设计者Jason Kottke),9710da1,2026年10月5日查阅,https://github.com/google/fonts/blob/main/ofl/silkscreen/OFL.txt ↩↩

  56. DaFont,“Pixel Operator Font”(Jayvee Enaguas作;“Bitmap (16 px)”;15个字体文件;241个字形;作者的说明,“Changed licence to Creative Commons Zero (CC0) 1.0”和“licensed under a Creative Commons Zero (CC0) 1.0”),2026年10月5日获取,https://www.dafont.com/pixel-operator.font 。说明中提到的仓库https://notabug.org/HarvettFox96/ttf-pixeloperator 于2026年10月5日返回HTTP 404。单一来源。 ↩↩↩↩

  57. Daniel Linssen,“m5x7”,itch.io(“use font size 16, 32, 48, etc”;“free to use but attribution appreciated”;“Asset license Creative Commons Zero v1.0 Universal”;平均宽度5,上伸部7,下伸部2,x高度5;其字符被描述为“uppercase and lowercase letters, including many accented letters, numbers and most common symbols”;“自定价格”的下载),2026年10月5日获取,https://managore.itch.io/m5x7 。字体文件没有下载。 ↩↩↩↩↩↩

  58. Daniel Linssen,“m3x6”,itch.io(“use font size 16, 32, 48, etc”;“free to use with attribution”;没有素材授权一栏;“自定价格”的下载),2026年10月5日获取,https://managore.itch.io/m3x6 。字体文件没有下载。 ↩↩↩↩↩

  59. Apple Developer Documentation,“Core Text”(“Create text layouts, optimize font handling, and access font metrics and glyph data”;iOS 3.2,macOS 10.8),2026年10月5日获取,https://developer.apple.com/documentation/coretext ↩

  60. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_hig.py,输出为measure_hig.out:已保存Typography页面中的表格,默认和最小尺寸,以及从xSmall到AX5的每张iOS Dynamic Type表(Body为14、15、16、17、19、21、23、28、33、40、47和53点;Caption 1为11、11、11、12、14、16、18、22、26、32、37和43)。 ↩↩↩↩↩

  61. Unicode联盟,“Unicode Standard Annex #14: Unicode Line Breaking Algorithm”,第57版,Unicode 18.0.0,2026-09-01(CJ类及“Treating characters of class CJ as class NS will give CSS strict line breaking; treating them as class ID will give CSS normal breaking.”),2026年10月5日获取,https://www.unicode.org/reports/tr14/ ↩↩

  62. Stardew Valley Wiki,“Modding:Dialogue”,最后编辑于2026年5月20日($b;肖像命令;Strings\SpeechBubbles),2026年10月5日获取,https://stardewvalleywiki.com/Modding:Dialogue ↩↩↩

  63. Stardew Valley Wiki,“Modding:Custom languages”,最后编辑于2026年1月3日(UseLatinFont、.fnt文件、推荐基准值为1.5的FontPixelZoom、FontApplyYOffset),2026年10月5日获取,https://stardewvalleywiki.com/Modding:Custom_languages ↩↩

  64. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_steam_languages.py,输出为measure_steam_languages.out,读取已保存Steam商店页面的语言表:Stardew Valley(https://store.steampowered.com/app/413150/)、Celeste(https://store.steampowered.com/app/504230/)、Sea of Stars(https://store.steampowered.com/app/1244090/)和Eastward(https://store.steampowered.com/app/977880/),2026年10月5日获取。 ↩↩↩↩↩

  65. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_apple_docs.py,输出为measure_apple_docs.out:第10节所引每个已保存Apple文档页面的标题、声明、摘要、平台和开头几段。 ↩↩

  66. Apple,Human Interface Guidelines,“Designing for games”(“To keep text comfortably legible on each device, ensure that it contrasts well with the background and uses at least the recommended minimum text size in each platform.”),2026年10月5日获取,https://developer.apple.com/design/human-interface-guidelines/designing-for-games ↩

  67. Apple Developer Documentation,“UIFontMetrics”(“A utility object for obtaining custom fonts that scale to support Dynamic Type”;iOS 11.0)、“scaledFont(for:)”、“scaledFont(for:maximumPointSize:)”和“scaledValue(for:)”(“Scales an arbitrary layout value based on the current Dynamic Type settings”;iOS 11.0),2026年10月5日获取,https://developer.apple.com/documentation/uikit/uifontmetrics ↩↩↩

  68. Apple Developer Documentation,“custom(:size:relativeTo:)”(一款“that scales relative to the given textStyle”的自定义字体;iOS 14.0),https://developer.apple.com/documentation/swiftui/font/custom(:size:relativeto:),以及“custom(:fixedSize:)”(一款“that does not scale with Dynamic Type”的自定义字体;iOS 14.0),https://developer.apple.com/documentation/swiftui/font/custom(:fixedsize:),2026年10月5日获取。 ↩↩↩↩↩

  69. Apple Developer Documentation,“ScaledMetric”(“A dynamic property that scales a numeric value”;iOS 14.0),2026年10月5日获取,https://developer.apple.com/documentation/swiftui/scaledmetric ↩↩

  70. Apple Developer Documentation,“dynamicTypeSize(:)”(iOS 15.0;“When limiting the Dynamic Type size, consider if adding a large content view with accessibilityShowsLargeContentViewer() would be appropriate.”),2026年10月5日获取,https://developer.apple.com/documentation/swiftui/view/dynamictypesize(:) ↩↩

  71. Apple Developer Documentation,“accessibilityShowsLargeContentViewer()”(iOS 15.0;“Don’t use the large content viewer as a replacement for proper Dynamic Type support.”),2026年10月5日获取,https://developer.apple.com/documentation/swiftui/view/accessibilityshowslargecontentviewer() ↩↩

  72. Apple Developer Documentation,“UIAppFonts”(“App-specific font files located in the bundle and that the system loads at runtime”;iOS 3.2),2026年10月5日获取,https://developer.apple.com/documentation/bundleresources/information-property-list/uiappfonts ↩↩

  73. Apple Developer Documentation,“CTFontManagerRegisterFontsForURL(:::)”(iOS 4.1),2026年10月5日获取,https://developer.apple.com/documentation/coretext/ctfontmanagerregisterfontsforurl(:::) ↩

  74. Apple Developer Documentation,“interpolation(:)”(Image;iOS 13.0),2026年10月5日获取,https://developer.apple.com/documentation/swiftui/image/interpolation(:) ↩

  75. Apple Developer Documentation,“resizable(capInsets:resizingMode:)”(“Sets the mode by which SwiftUI resizes an image to fit its space”;iOS 13.0),2026年10月5日获取,https://developer.apple.com/documentation/swiftui/image/resizable(capinsets:resizingmode:) ↩

  76. Apple Developer Documentation,“baselineOffset(:)”(Text;“Sets the vertical offset for the text relative to its baseline”;iOS 13.0),2026年10月5日获取,https://developer.apple.com/documentation/swiftui/text/baselineoffset(:) ↩

  77. Apple Developer Documentation,“typesettingLanguage(:isEnabled:)”(iOS 17.0;关于与设备界面语言不一致的文字的段落;“this language does not affect text localization.”),2026年10月5日获取,https://developer.apple.com/documentation/swiftui/view/typesettinglanguage(:isenabled:) ↩↩

  78. Apple Developer Documentation,“NSParagraphStyle.LineBreakStrategy”(iOS 9.0)及其属性standard(iOS 14.0)、pushOut(iOS 9.0)和hangulWordPriority(iOS 14.0),2026年10月5日获取,https://developer.apple.com/documentation/uikit/nsparagraphstyle/linebreakstrategy-swift.struct ↩

  79. Apple Developer Documentation,“accessibilityValue(:)”(“Use this method to describe the value represented by a view, but only if that’s different than the view’s label.”;LocalizedStringResource形式为iOS 16.0),https://developer.apple.com/documentation/swiftui/view/accessibilityvalue(:),以及“accessibilityLabel(:)”(iOS 16.0),https://developer.apple.com/documentation/swiftui/view/accessibilitylabel(:),2026年10月5日获取。 ↩↩↩

  80. Apple Developer Documentation,“accessibilityDirectTouch(:options:)”(iOS 17.0;“Direct touch areas passthrough touch events to the app rather than being handled through an assistive technology, such as VoiceOver.”;该区域须先激活),2026年10月5日获取,https://developer.apple.com/documentation/swiftui/view/accessibilitydirecttouch(:options:) ↩↩

  81. Apple Developer Documentation,“updatesFrequently”(AccessibilityTraits;iOS 13.0;“Use this trait when you want an assistive technology to poll for changes when it needs updated information.”),2026年10月5日获取,https://developer.apple.com/documentation/swiftui/accessibilitytraits/updatesfrequently ↩

  82. Apple Developer Documentation,“AccessibilityNotification.Announcement”(iOS 17.0;accessibilitySpeechAnnouncementPriority属性),2026年10月5日获取,https://developer.apple.com/documentation/accessibility/accessibilitynotification/announcement ↩↩

  83. Apple Developer Documentation,“accessibilityReduceMotion”(iOS 13.0;“If this property’s value is true, UI should avoid large animations”),2026年10月5日获取,https://developer.apple.com/documentation/swiftui/environmentvalues/accessibilityreducemotion ↩↩

  84. Apple Developer Documentation,“AccessibilityComponent”(RealityKit;“A component that stores accessibility information for an entity”;其名称是“VoiceOver says aloud when it describes the entity”;iOS 17.0),2026年10月5日获取,https://developer.apple.com/documentation/realitykit/accessibilitycomponent ↩↩↩↩

  85. Apple Developer Documentation,“Improving the Accessibility of RealityKit Apps”(“set the properties on each visible Entity in your scene”),2026年10月5日获取,https://developer.apple.com/documentation/realitykit/improving-the-accessibility-of-realitykit-apps ↩

  86. Apple Developer Documentation,“RealityView”(iOS 18.0),2026年10月5日获取,https://developer.apple.com/documentation/realitykit/realityview ↩

  87. Apple Developer Documentation,“XCUIVoiceOverService”(“Provides programmatic control of VoiceOver for UI testing”;iOS 27.0),2026年10月5日获取,https://developer.apple.com/documentation/xcuiautomation/xcuivoiceoverservice ↩↩↩

  88. 941Kit源码,Sources/Kit941/Tokens/Typography/FontFamily.swift,提交50bc657(2026年9月28日),第21到63行(case custom(named: String),以34、22、17、13和12点的固有尺寸,分别对应display、title、body、label和caption,通过Font.custom(_:size:relativeTo:)渲染),2026年10月5日查阅;私有仓库。 ↩↩

  89. Kiradex源码,server/app/names.py(第7到14行,FIRSTS和SECONDS各20个词,以及由两个首字母大写的词加四位数字组成的昵称格式),于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库)。 ↩↩

  90. Blake Crosley,“A Pixel Town Alive When Nobody Is Online: NPCs and Ghosts”,blakecrosley.com,2026年10月5日(23句预设台词,其中14句供玩家使用,9句供小镇自己的收藏者使用,以及每位玩家每2.0秒一句),https://blakecrosley.com/blog/pixel-art-town-alive-when-nobody-is-online ↩↩

  91. Kiradex,docs/WORLD.md,第5节“The stack”,第524和525行(速率限制:“eight moves a second, a line every two seconds”),于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库)。 ↩↩

  92. Kiradex源码,于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库):Kiradex/World/PlazaStage.swift,第199到203行(舞台的.gesture(DragGesture(minimumDistance: 0)),变化时调用touched,结束时调用lifted),第329到359行(touched(_:in:):没有摇杆时,立即走向手指下方的地面,并跟随手指跨越图块,而触摸收藏者或信息亭图块时则返回,等待抬起),第364到380行(lifted(_:in:):没有行走也没有操纵的触摸即为点按;在另一位收藏者上它设置tapped,在信息亭图块上它调用onKiosk,否则运行rig.walk(to: tile)),第31和32行(onKiosk,“The kiosk tapped: the town’s board.”),第82行(stickKey,"world.stick"),测试启动参数-plazaFacingDemo(第255到270行的.task),以及PlazaView中读取-plazaTell的tellOnArrival(第515到522行);KiradexUITests/KiradexUITests.swift第384和387行,行走测试在那里按舞台框的比例位置点按。 ↩↩↩↩

  93. Kiradex源码,Kiradex/World/PlazaRig.swift,于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库):第432行的fit(viewSize:displayScale:tilesTall:fitsWhole:)(pixelScale为视图像素高度除以tilesTall × TileMap.tileSize,即20 × 16 = 320后向下取整,第439行;TileMap.tileSize在TileMap.swift第60行为16),以及第479行的headPoint(of:)(行走者的位置加上字表帧高度的一半,再加4,第482和483行)。到12:42该文件又有改动;那时这两个函数都没有变。 ↩↩

  94. Kiradex源码,于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库):Kiradex/World/PlazaStage.swift第311行(舞台上的.ignoresSafeArea(),其GeometryReader,即第121到294行,容纳气泡的叠加层),整个文件中没有读取安全区域边距之处;Kiradex/World/WorldHUD.swift第26和27行(世界的根视图WorldView上的.statusBarHidden()和.persistentSystemOverlays(.hidden))和第40到42行(界面元素位于“over its stage and inside the safe area, which on the Duo keeps it clear of the cameras and the side rail”);以及Kiradex/Views/Card/CardViewer.swift第19到24行,卡片舞台之外的一个GeometryReader把proxy.safeAreaInsets传入忽略安全区域的CardStage,使得“the card rests inside the safe area, clear of the Duo’s outer camera and side rail”,第56和57行有同样的两个修饰符。在Kiradex/中搜索safeAreaInsets,只找到CardViewer那一行。 ↩↩↩↩↩

  95. Blake Crosley,“Xcode 27.1 Beta: Your App in the iPhone Duo Simulator”,blakecrosley.com,2026年9月21日,2026年10月2日更新(在iPhone Duo模拟器的外屏和内屏上运行应用;合上状态,链接27.1 SDK,466×678点的窗口中,导航栈内的内容测得为382×562点,安全区域边距为顶部82、底部34、尾侧84,即侧边轨,那次探针在该边缘有工具栏和标签栏,并显示状态栏),https://blakecrosley.com/blog/xcode-27-1-beta-iphone-duo-simulator ↩↩↩↩

  96. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_figure_width.py,输出为measure_figure_width.out,于太平洋时间12:36针对提交334fbfe加上当天未提交修改的Kiradex重新运行,输出未变。输入只从Kiradex读取:Kiradex/Resources/World/characters.json("cell": [32, 40],帧和朝向)、scripts/forge/rig.py(第53行,CELL_W, CELL_H = 32, 40)以及身体字表fig-body.png(1,376×160像素,43帧×4个朝向)。在站立帧idle_a中,人物在每个朝向都高30纹素,最宽一行在朝下或朝上时宽16,朝左或朝右时宽13。 ↩↩↩

  97. 作者测量,2026年10月5日:本文作者证据文件夹中的measure_ios27_iphones.py,输出为measure_ios27_iphones.out,于太平洋时间12:36重新运行,输出未变。它读取作者Mac上随Xcode 27.0安装的iOS 27.0模拟器运行时(24A434)的xcrun simctl list runtimes -j,以及每种iPhone设备类型的capabilities.plist(main-screen-width、main-screen-height、main-screen-scale):33款iPhone,从iPhone 12 mini和13 mini(3×下1,080×2,340像素,360×780点)到iPhone 16、17和18 Pro Max(440×956点),其中三款为2×,即iPhone SE(第2代和第3代,375×667点)和iPhone 11(414×896)。这是模拟器的设备列表,不是Apple的兼容性页面,后者没有获取。另外只读取了提交334fbfe时的Kiradex项目文件:应用目标两种配置中的TARGETED_DEVICE_FAMILY = 1(iPhone)(12:28读取时为第1559和1705行),以及项目两种配置中的IPHONEOS_DEPLOYMENT_TARGET = 27.0(第1608和1670行),应用目标没有覆盖它。 ↩↩↩↩↩↩↩

  98. 作者测量,2026年10月5日:本文作者证据文件夹中的probe_line_start_glyphs.swift,用swiftc -O编译并在macOS 27.0(版本26A428)上运行,不是iOS,输出为probe_line_start_glyphs.out。它从已保存规范的附录表中读取JLREQ 3.1.7的十个行首类别(83项:cl-02 16、cl-03 4、cl-04 6、cl-05 3、cl-06 2、cl-07 2、cl-09 6、cl-10 1、cl-11 40、cl-29 3),向Core Text询问PixelMplus10 Regular映射了哪些(CTFontGetGlyphsForCharacters,无后备):54项,未映射的有cl-03的〜 U+301C、゠ U+30A0和en dash,cl-02的⦆、〙和〗,cl-04的‼ ⁇ ⁈ ⁉,cl-09的〻,以及cl-11的18个小写假名(ゕ、ゖ和U+31F0到U+31FF)。它在10像素下、关闭抗锯齿和字体平滑,绘制该字体映射的全部7,223个码位,并把像素完全相同的单元格归组:在54项中,‐ U+2010与U+2011一致,: U+003A与U+A789一致,没有其他项有孪生。在50像素(m = 5)下、开启抗锯齿并关闭平滑时,ー U+30FC绘制出200个着墨设备像素,左端带一个钩,一 U+4E00为250个,两者不同,都没有中间调像素。每个可打印ASCII字符的步进都是5个字体像素。10月5日稍后扩展,早先的脚本和输出保留在旁,输出中原有各行均未改变:Halfwidth and Fullwidth Forms区块,即U+FF01到U+FFEF中,兼容规范化(NFKC)结果为所列字符之一的24个字符(! ) , . : ; ? ] } ⦆ 。 」 、 ・ ァ ィ ゥ ェ ォ ャ ュ ョ ッ ー),以及作者添加的~ U+FF5E:该字体映射了25个中的24个,只有⦆ U+FF60除外,全角的宽10像素,半角的宽5像素,这24个在字体的码位中都没有像素完全相同的孪生,所以比较集合为75个字符:54项包含51个字符,因为)、〕和]同时列于cl-02和cl-29,加上24个形式得75(探针自己最后一行的54 + 24 = 78计的是条目)。 ↩↩↩↩

  99. 作者测量,2026年10月15日:本文作者证据文件夹中的measure_cell_masks.py,输出为measure_cell_masks.out和摘要列表canon_cell_masks.txt,针对为本文保存的pret文件运行(pokeemerald的graphics/fonts,17张字表,以及graphics/text_window,21张;pokecrystal的gfx/frames,10张;pokecrystal和pokered的gfx/font/font.png)。每张字表解码为RGBA;与字表左上角像素不同的像素置位;单元格从左上角起按8×8划分;空白和全满的单元格排除;掩码的摘要为其8个行字节的SHA-256,最左侧像素为最高位。japanese_normal.png,128×512像素:1,024个单元格,430个空白,保留594个,其中478个不同。全部50张字表:保留7,746个单元格,1,484个不同的摘要。 ↩↩

  100. Blake Crosley,“Pixel-Art People: Characters and a Creator on iPhone”,blakecrosley.com,2026年10月3日,2026年10月5日查阅:关于构建21到33的人物,“The cell is 32 by 32; the figure is 26 tall to the crown of the head, 27 with most hair (28 with buns, spikes or the puff), and about 14 wide at the shoulders”;在其关于构建34的更新中,“The cell is 32 by 40 now, with eight rows of headroom and two under the feet, and the figure 30 pixels”;以及清单中为气泡和3D卡片准备的头部和手部锚点,“though both still take their place from the cell’s height for now”,https://blakecrosley.com/blog/pixel-art-characters-on-iphone ↩↩

  101. 作者对提交fc9d4876(refs/heads/main)时Kiradex工作树的阅读,2026年10月15日太平洋时间22:29:Kiradex/World/PlazaStage.swift(当天17:13修改),第373行(.modifier(StageAccessibility(covered: covered, trace: stepTrace))),第520行(private struct StageAccessibility: ViewModifier),第511到513行(注释:舞台是一个辅助功能元素,“The plaza”,以玩家的最后一步为值,或者在某个地点覆盖其上时不存在),第525行(content.accessibilityRepresentation {),第526行(if !covered {),第533行(.accessibilityLabel("The plaza"))和第534行(.accessibilityValue(trace))。本文其他对Kiradex的阅读都是10月5日的,并已注明日期。 ↩

  102. Kiradex源码,server/app/npcs.py,于2026年10月5日太平洋时间12:33读取,提交334fbfe(文件日期为10月3日;私有仓库):DEFINITIONS中的小镇四位收藏者(第36到58行),Moss Keeper 1999、Prism Scout 2024、Comet Pocket 3030和Dawn Sprout 4040,他们的台词是LINES中的索引;每人在8到20秒后说出第一句(第77行),此后每20到40秒说一句(第108行)。server/app/main.py第231行把玩家的一句台词广播给整个房间,包括玩家自己(broadcast的except_id未设置;rooms.py第182行),12:33读取。 ↩↩

  103. Kiradex源码,KiradexUITests/KiradexUITests.swift,于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库);该文件日期为10:32,读取期间没有变化:第228行(“Me”标签页的me-handle保存昵称,两个词加一个数字),从第432行起的testTheCollectorFacesTheWayItWalks(读取每个朝向的舞台值),从第502行起的testThePlazaKeepsTime(读取plaza-status并检查其前缀,第509行),从第523行起的testTheChimneysSmoke(以-airProbe启动,等待舞台的value CONTAINS 'smoke 18 ',第531行),testVisitingARoomIsAVisit(第469到472行)和testYourRoomHasYourBinders(第604到607行),它们等待room-status的头几个词,testTheShowHallHasEveryonesCards(第490到494行),它等待hall-status,testTheTownHasCollectors(第672到676行),它等待plaza-status中“here”的人数达到五或以上,以及从第689行起的testThePlazaHasCompany(除非KIRADEX_PLAZA=1即服务器在运行,否则跳过,按其跳过信息还需要一位同伴,第690和691行;等待plaza-status中的人数,从“Say”菜单选择“Hi!”,并等待5秒出现气泡Said: Hi!,第714行)。 ↩↩↩↩

  104. Kiradex源码,server/requirements.txt(fastapi、uvicorn[standard]、websockets、httpx和pytest,没有字体库)和server/tests/(test_rooms.py),于2026年10月5日太平洋时间12:28读取,提交334fbfe加上当天未提交的修改,来自当天正在修改的工作树(私有仓库)。 ↩

  105. Blake Crosley,“Pixel-Art Routes: Map Connections and Districts on iPhone”,blakecrosley.com,2026年10月5日,第7节,检查13(仓库中的一份拒绝列表及其测试,每个区域名、招牌和字符串都必须通过),https://blakecrosley.com/blog/pixel-art-world-beyond-the-plaza ↩

相关文章

像素艺术人物:iPhone 上的角色与捏人器

顶尖的像素艺术游戏如何塑造和装扮笔下的人物?我们逐一实测,再据此在 iPhone 上重做了 Kiradex 的收藏者、衣橱与捏人器界面。

56 分钟阅读

像素艺术建筑:在 iPhone 上盖房子、大厅与室内

宝可梦、SNES 时代的 RPG 和《星露谷物语》如何搭建房屋、室内与楼层,全部从源码实测;以及 Kiradex 的 iPhone 小镇如何盖起真正的建筑。

55 分钟阅读

App Review 究竟在看什么——就公开资料所知:2026年10月

从公开记录出发,梳理2026年10月的 Apple App Review:谁在审核、要等多久、什么会导致被拒以及拒绝信的原文措辞、2025至2026年的规则变动,以及一款卡牌收藏应用在首次提交前照着这份清单做的红队演练。

36 分钟阅读