像素藝術介面:文字框、字型與日文
《寶可夢 綠寶石》(Pokémon Emerald)的對話框是一個 30 乘 6 圖塊的外框,橫跨整個 240 像素寬的螢幕、佔螢幕高度的 30%,框內是一個 27 乘 4 圖塊的文字視窗,容納兩行 16 像素的字型;《水晶》(Pokémon Crystal)的對話框是 20 乘 6 圖塊,兩行各 18 個字元。123 《綠寶石》依速度設定每 8、4 或 1 個影格印出一個字元,在 60 赫茲下就是每秒 7.5、15 或 60 個;《水晶》的快速設定與 Final Fantasy VI 也是一個影格印一個字。14 《綠寶石》的編劇逐行手動斷行:在 24,100 行腳本文字中,最寬的一行是 208 像素,而視窗寬 216 像素,沒有任何一行溢出。1 一套涵蓋標準字集的日文像素字型大約要畫七千個字:JIS X 0208 有 6,879 個字元,Misaki 在 8 乘 8 的格子裡全部畫齊,PixelMplus10 在 10 像素網格上只缺三個碼位,而 Final Fantasy Pixel Remaster 的製作人說他團隊新做的日文字型有「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 能描述的舞台。
重點摘要
- 文字框佔螢幕下方三分之一,兩行。 《綠寶石》的對話外框是 30 乘 6 圖塊(240 乘 48 像素,佔螢幕高度 30%),框內是一個 27 乘 4 圖塊、容納兩行 16 像素文字的視窗;《水晶》的文字框是 20 乘 6 圖塊,佔高度 33%,兩行各 18 個字元,《紅》(Pokémon Red)的也同樣是 20 乘 6。《綠寶石》的文字框沒有姓名欄,它的 14,412 個腳本段落中有 934 個(6.5%)在文字裡寫出說話者的名字。12312
- 速度是影格數。 《綠寶石》與《火紅》(Pokémon FireRed)每個字元花 8、4 或 1 個影格(在 60 赫茲下每秒 7.5、15 或 60 個),而在印字過程中按過 A 或 B 之後,只要按住就是一個影格一個字元;《水晶》與《紅》等待 5、3 或 1 個影格(每秒 12、20 或 60 個);Final Fantasy 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 點)中,一個 10 像素的 em 在 3× 下只有 40 點是整數,8 像素的 em 是 16 與 40 點,12 像素的 em 是 16、28 與 40 點。Apple 要求自訂字型實作系統字型所具備的行為,Dynamic Type 也在其中,並且希望 App 的文字最好能讓使用者放大到至少 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 秒時顯示。地點的那一行字放在螢幕底部的一個米色文字框裡。App 只有英文,預設台詞以英文存放在伺服器上,工坊的告示牌是一塊 12 乘 11、只有墨色塗畫而沒有字母的木板,而 VoiceOver 把整個舞台當成一個元素朗讀,其值是走路測試的步伐軌跡。11242526272829 細節在第 10 節;第 12 節的規格書是以相對於它的差異寫成的。
下面每一個 Pokémon 的數字,都量自 pret 反編譯專案的淺層複製:pokeemerald 731ad5b(2026年10月1日)、pokefirered 037335f、pokecrystal 5beda23 與 pokered d2704a6,由同一支 Python 腳本 measure_gen_text.py 量出,輸出就存放在它旁邊;Final Fantasy 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 把延遲減一後存成印字器的速度;每印出一個字元,印字器就把計數器設成那個存下的速度,每個影格減一,而在計數器歸 0 後的下一個影格印出下一個字元。所以一個字元花的時間就是延遲本身: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
按住按鈕會讓較慢的設定加速。在允許加速的訊息中(gTextFlags.canABSpeedUpPrint),若在某個字元的延遲倒數時新按下 A 或 B,就會設定 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 像素的一行依速度設定(sWindowVerticalScrollSpeeds)每個影格上移 1、2 或 4 像素,所以一次捲動要花 16、8 或 4 個影格。113
每款量測過的遊戲的文字速度,以及規格書提議的兩種設定;由 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,三乘三個 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 的圖,三乘三個硬體圖塊,也就是九宮格)。23 graphics/text_window/ 裡那二十張 24 乘 24 的圖,是選單視窗可選擇的外框。對話框,也就是玩家記得的那個文字框,是由 56 乘 16 像素的 message_box.png 畫成的一個 30 乘 6 圖塊、底邊鏡像的外框,而不是一張 24 乘 24 圖的九宮格。136 九宮格這個想法本身成立,選單也確實這樣用;把它歸給對話框則不成立。
兩個對話框的圖塊網格,不含任何遊戲美術;由 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 每一個斷行都是手動放上的斷行碼。遊戲從不自動換行;它信任編劇。
文字框只有一個視窗,沒有姓名欄。編劇需要說話者時,就把名字和冒號直接打進那一行,例如未白鎮(Littleroot Town)的「MOM: {PLAYER}, we’re here, honey!」(媽媽:{PLAYER},我們到了,親愛的!)。在 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.7 fps),並註明一個影格「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 像素的一行、位於底部的文字框)並不閃爍:它以 36 個影格為一個循環,經過三個位置,從靜止位置起 0、1、2 與 1 像素(程式碼是位移點陣圖的來源列,依我的解讀,這會讓楔形在螢幕上往上抬;我沒有實際執行過)。113 第 12 節的規格書沿用《綠寶石》的步進,但不沿用它的方向。
3. Final Fantasy VI:由您重新上色的視窗,以及會自動換行的字型
Final Fantasy VI 把視窗當成玩家的東西。這裡的數字來自 everything8215/ff6 反組譯專案的 8130132(2026年9月14日),它收錄的是程式碼與擷取清單,而不是 ROM 的圖像,所以尺寸是從位元組範圍與讀取它們的程式碼得來的。304
八種視窗,每種七個顏色
英文與日文的擷取清單都把視窗圖像列為 0xED0000 到 0xED1BFF,共 7,168 位元組:八種樣式,每種 896 位元組,各是 28 個 8 乘 8、每像素 4 位元的圖塊。它們的調色盤緊接在後,位於 0xED1C00 到 0xED1CFF,是八組 16 色調色盤。442 設定畫面列出 8 種視窗圖樣(ConfigWindowNumPos)。443
視窗的顏色由玩家決定。LoadWindowPal 從存檔記憶體 $1d57 讀取圖樣的顏色,每種圖樣 14 位元組,也就是七個 2 位元組的顏色,設定畫面則以 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
與《綠寶石》相比,Final Fantasy VI 示範了做同一個文字框的另一種方式:《綠寶石》每一個斷行都手寫,讓玩家選擇選單的外框,但從不讓玩家選對話框的外框;Final Fantasy VI 在程式碼中換行,讓玩家重新為視窗上色,並以硬體做出漸層。《綠寶石》的原野文字框位於底部,Final Fantasy VI 的在頂部或底部,兩者都以固定的影格數印字。
Chrono Trigger
我原本想把 Chrono Trigger 和 Final Fantasy VI 並排比較,卻做不到。我沒有複製到任何公開的反組譯專案,而 Data Crystal 上這款遊戲的 ROM 對照表在擷取時傳回 HTTP 403,所以本文關於文字框的內容沒有任何一項來自它(第 11 節)。49
4. 8、10 與 12 點的日文
Kiradex 是一款卡片收藏者用的 App,而圖鑑(Dex)已經用系統字型顯示日文卡名,英文列在下方。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
Misaki(美咲フォント),由 Num Kadoma 製作,是最小的一套。它的頁面稱之為「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 像素,位於一個 ascent 6、descent 2 的 8 乘 8 格子中。在 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 個半形),ascent 7、descent 1,其餘量測結果相同。651
k8x12:8 乘 12
k8x12 出自同一位作者,維持 8 點的寬度,加高了字身:「8×12 ドットの日本語ビットマップフォント」(8 乘 12 點的日文點陣字型),JIS 第一與第二水準。它有兩種變體:假名畫得較高的 k8x12L,以及非漢字高 8 點的 k8x12S,頁面說後者是考量到舊遊戲主機的限制而做的:「8×8 ドット単位でグラフィックを扱うような、昔のゲーム機等の制限を意識しています」(意識到以 8 乘 8 點為單位處理圖像的舊遊戲機等的限制)。52
從 BDF 量測,k8x12 是一個 ascent 10、descent 2 的 8 乘 12 格子:7,170 個字元,168 個寬 4 像素,7,002 個寬 8 像素。沒有任何漢字填滿格子;填滿格子的全形字形,在 k8x12 與 k8x12L 中有 53 個,在 k8x12S 中有 45 個。三者都畫齊了 JIS X 0208 的全部 6,879 個字元。6
PixelMplus:10 與 12,以輪廓呈現
PixelMplus 由 itouhiro 製作(2013),是另一種做法:看起來像點陣字型的 TrueType 輪廓,沒有內嵌點陣圖(「埋め込みビットマップは なし」,沒有內嵌點陣圖),收錄 JIS 第一與第二水準的全部漢字(「JIS第1・第2水準のすべての漢字を収録」,收錄 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:不在網格上的點狀字型
DotGothic16 由 Fontworks 製作,在 Google Fonts 上以 SIL Open Font License 1.1 授權,沒有保留字型名稱(Reserved Font Name),列出的子集是 cyrillic、japanese、latin 與 latin-ext,加入日期是 2020-12-15。53 它有 9,362 個字形,畫齊 JIS X 0208 的全部 6,879 個字元。7 但它是以分離的點畫成的,不是像素。在它的 A 中,輪廓的 x 邊緣成對出現:在每 em 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(build 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 個取樣輪廓座標中有 0 個偏離它的 10 像素網格,Bold 是 1,000 個中 0 個;Misaki Gothic 是 848 個中 0 個;k8x12 是 988 個中 0 個;PixelMplus12 在允許 0.39 像素的偏移後是 1,192 個中 0 個,而 DotGothic16 的 1,544 個全部偏離。7 PixelMplus12 的偏移在它的原始座標裡就看得出來:它的 H 從 y −33 延伸到 717(每 em 1,000 單位),網格是每像素 83.3 單位,所以它的基線落在一列網格內 0.396 像素處;探測程式的估計值 0.39,來自把座標的小數部分分進寬度為五十分之一像素的區間,也是下文渲染時所平移的數值。19733
在每一套量測過、能顯示日文的字型中,一個日文字元都是拉丁字元的兩倍寬:Misaki 與 k8x12 是 8 對 4,PixelMplus 是 10 對 5 與 12 對 6。7 經典作品也落在同一個範圍:《綠寶石》的日文在 16 像素的一行中寬 8 像素,《火紅》的字形表大多是 10,Final Fantasy 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 上世界本身的密度來說,一個 texel 是 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 單位的網格上有 495 與 130 單位的 y 值),它的 A 寬 6 像素。在 JIS X 0208 中,它畫出 21 個非漢字。719
另外三套常被推薦用於像素遊戲的字型沒有量測,我寧可直說,也不憑記憶描述它們。
Pixel Operator,由 Jayvee Enaguas 製作:作者在 DaFont 上的說明寫著「Changed licence to Creative Commons Zero (CC0) 1.0」(授權改為 Creative Commons Zero (CC0) 1.0),並說這套字體「licensed under a Creative Commons Zero (CC0) 1.0」(以 Creative Commons Zero (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」(素材授權 Creative Commons Zero v1.0 Universal),並給出平均寬度 5、ascender 7、descender 2、x-height 5。它把字元描述為「uppercase and lowercase letters, including many accented letters, numbers and most common symbols」(大寫與小寫字母,包括許多帶重音符號的字母、數字與大多數常用符號)。57
m3x6,出自同一位作者:「use font size 16, 32, 48, etc」(請使用字級 16、32、48 等)與「free to use with attribution」(署名即可免費使用);它的頁面沒有素材授權欄位。58 兩個檔案都沒有下載,因為兩者都放在自訂價格的下載後面,所以兩者都沒有量測。5758
這是世界篇的第三處不精確。它的文字計畫指定 m5x7 為字體。23 依它自己頁面的描述,m5x7 是拉丁字型,頁面上沒有任何地方說它有假名或漢字;一個要顯示日文的 App 不能用它來排世界裡的文字。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(build 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
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 有些列與欄被加倍,有些沒有,所以原本一像素寬的筆畫,會變成一或兩像素寬。
依尺寸的部分像素,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
哪些 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日的 Working Group Note)在 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 的嚴格斷行;當作 ID 類處理,會得到 CSS 的一般斷行。)61
Core Text 怎麼做
measure_coretext.swift 在 macOS 27.0(不是 iOS)上執行,用 CTFramesetter 以 8、10、12、14、16、18 與 20 em 的寬度排出一段 131 個字元的日文段落,並把第一行之後每一行的第一個字元,拿去對照它自己的一份行首禁則字元清單:部分閉括號、日文的逗號與句號、中點、冒號與分號、問號與驚嘆號、長音符號、小寫假名與々。7 那份清單並不是 JLREQ 規則所列類別的子集,只是與它們有所重疊。它的 40 個字元中有七個,,.:;?!與),是日文文字在 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 一般斷行,而未標記的執行則表現得很嚴格。2161 這與我的預期正好相反,因為標記指明的就是語言本身,這也是規格書在沒有擷取驗證之前不為文字框文字加上標記的原因。
關於量測本身有兩點說明,兩者都來自把腳本與輸出對照閱讀。寬度以 em 為單位,也就是字型的完整尺寸,所以對於假名寬 8 像素、em 為 12 像素的 k8x12 來說,每個寬度容納的假名數是數字所示的一倍半;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
Final Fantasy Pixel Remaster:字型才是真正的工程
Pixel Remaster 的主機版新增了像素字型選項,製作人 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」(約 7,500 個字元)。238 數字相同,來源更好。
七千五百大致相當於 JIS X 0208 的 6,879 個字元,也就是一般的涵蓋目標;這個比較是我做的。533 一款每一行台詞在出貨前就已固定的遊戲可以少畫一些,就像 Final Fantasy VI 的日文 ROM 只用了 1,248 個對話字母(第 3 節),但這樣一來每一個字串都必須與字型比對;一套涵蓋整個字集的字型,對從中取字的任何字串都不需要做這種比對。4對一款小型 App 來說,這個教訓與第 4 節的免費字型從另一面所教的相同:對於會持續增加的文字,日文像素字體只有在有人已經畫好那七千個字、而授權也允許您隨 App 出貨時,才算是已解決的問題。
現代像素遊戲出貨了什麼
在 Steam 上,2026年10月5日,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;Final Fantasy VI 的視窗位於第 1 或第 18 列 |
| 拉丁字形 | 寬 4 到 12 像素,位於高 8 到 16 像素的格子中。 | 《綠寶石》6 乘 16 的格子,實際文字中每字元 5.25 像素;Final Fantasy VI 在 16 乘 11 的格子中 4 到 12;《水晶》固定 8 乘 8 |
| 日文字形 | 8 到 12 像素,約為同一套字型中拉丁字母的兩倍。 | Misaki 8 乘 8,墨跡 7 乘 7;《綠寶石》在 16 的一行中寬 8;《火紅》的字形表與 PixelMplus10 為 10;Final Fantasy VI 與 PixelMplus12 為 12;在每一套量測過、能顯示日文的字型中都是 8 對 4、10 對 5、12 對 6 |
| 行長 | 一行 18 到 41 個字元;寫到寬度為止,而不是把它填滿。 | 《水晶》18;《綠寶石》216 像素的視窗以 5.25 像素計可容納 41,編劇在 95% 的行數停在 38;Final Fantasy VI 220 像素的一行以 12 像素計可容納 18 個日文字 |
| 斷行 | 要嘛由編劇逐行斷行,要嘛由程式碼依單字換行;絕不兩者並用。 | 《綠寶石》24,100 行手動斷行,沒有一行超過 208 像素;Final Fantasy VI 的 CalcTextWidth 與 NewLine |
| 速度 | 以時間計算字元數:每秒 60 個為上限(《綠寶石》與《水晶》的快速設定,以及 Final Fantasy VI 的一個影格一個字母),中速 15 到 20,慢速 7.5 到 12。 | 《綠寶石》7.5、15 與 60,按住 60;《水晶》與《紅》12、20 與 60;Final Fantasy VI 60 |
| 續行標記 | 讓標記以緩慢的節奏移動:每 150 毫秒一步,一個循環約 0.6 秒。 | 《綠寶石》的 0、1、2、1 像素,每 9 個影格一步,每循環 36 個影格;《水晶》的 ▼ 與邊框圖塊交替閃爍 |
| 外框 | 外框是圖塊,不是圖畫:一個角、一條邊與一個中心,重複排列。讓玩家從少數幾種中選擇。 | 《綠寶石》20 種可選的 24 乘 24 選單外框,以及底邊鏡像的 14 圖塊對話長條;《水晶》8 種外框,每種 6 個 1 位元圖塊;Final Fantasy 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;Pixel Remaster 的「around 7,500」(約 7,500);Final Fantasy VI 日文 ROM 的 1,248 個對話字母;Misaki、k8x12 與 DotGothic16 畫齊 JIS X 0208,PixelMplus 只缺三個碼位 |
| 授權 | 只有在手上握有授權全文、讀過條款之後,才隨 App 出貨一套字體。 | 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 場景,搭配一台正交攝影機,每個 texel 對應整數個裝置像素,並在上方以 SwiftUI 呈現使用者要閱讀的一切。23 世界篇決定對話文字「lives in SwiftUI at UI scale rather than in the world」(以 UI 尺度存在於 SwiftUI 中,而不是在世界裡),本節保留這個決定並補上 API:Apple 對自訂字體有什麼要求、內建的像素字型如何載入、如何以捨入的方式尊重 Dynamic Type、日文如何斷行,以及 VoiceOver 如何朗讀一個由單一 RealityKit 視圖構成的舞台。下面每一項可用版本都來自 Apple 的文件頁面,保存於2026年10月5日;這些程式碼都還沒有建置過,本節中的每一項成本也都沒有在手機上量測過。65
Apple 對文字的要求
《人機介面指南》的 Typography 頁面給 iOS 與 iPadOS 的預設文字大小是 17 點,最小 11 點,並要求 App「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 App 則為 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:)與scaledValue(for:),後者「Scales an arbitrary layout value based on the current Dynamic Type settings」(依目前的 Dynamic Type 設定縮放任意版面數值)。67- SwiftUI 中的
Font.custom(_:size:relativeTo:)建立一個「that scales relative to the giventextStyle」(相對於指定textStyle縮放)的自訂字型,從 iOS 14.0 起提供。68 ScaledMetric,「A dynamic property that scales a numeric value」(縮放數值的動態屬性),從 iOS 14.0 起提供。69Font.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(Small,15 點,是唯一剛好落在一半的值: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 規格書只限制必須維持一行的對話泡泡,絕不限制文字框。
內建字型
字型隨 App 套件出貨,並在 UIAppFonts 中列名,即「App-specific font files located in the bundle and that the system loads at runtime」(位於套件內、由系統在執行時載入的 App 專屬字型檔)(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 個裝置像素;依我的解讀,resizable(capInsets:) 搭配 .tile 只有在平鋪落在整數裝置像素上時才同樣適用,而這一點沒有人量測過。
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」(可能包含與裝置 UI 語言不符之特定語言文字的 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」(直接觸控區域會把觸控事件直接傳給 App,而不是交由 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」(UI 應避免大幅度的動畫)。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日從程式庫讀取,位於 commit 334fbfe 加上當天未提交的修改,我從未執行或修改它;我閱讀時這個 App 正在被編輯(第 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 有 11 個。7 - 對話泡泡。
Text(text).kit941Font(.caption, weight: .bold),墨色字在WorldInk.paper上(一個只有兩種固定顏色的美術表面),左右以套件的小間距、上下以特小間距留白,加上 2 點的墨色邊框與.fixedSize(),所以是一行。它放在rig.headPoint上方一個大間距的位置,在那句話出現未滿 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」(x,y 朝向)表示的最後一步,保留它是因為「the walks read it, since nothing else the simulator logs reaches them」(走路測試會讀它,因為模擬器記錄的其他東西都傳不到它們那裡)。App 的原始碼中沒有任何地方出現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 內側顯示器上,一個 texel 是 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 時,五個 texel 的字型是 13.3 點,k = 6 時是 10 點,低於 HIG 的 11 點下限)。23 它只約束在 k = 6 時小於 5.5 texel 的字型(以每 texel 2 點計為 11 點);8 texel 的 em 在 k = 6 時是 16 點。1133 結論,也就是文字以 UI 尺度放在 SwiftUI 中,基於另一個理由成立:在世界密度下,以每 texel 8 個裝置像素的手機來說,10 像素的 em 是 26.67 點,在 18 Pro Max 上文字框的一行只剩 29 個拉丁字元。11 以圖塊來衡量,經典作品的文字框很窄。一個 16 個硬體圖塊寬、128 像素的文字框,容納 16 個 8 乘 8 字型的字元,以《綠寶石》拉丁字元平均 5.25 像素計約 24 個(以最壞情況 6 像素計為 21 個),16 個《綠寶石》的 8 像素日文字,以及 10 個 Final Fantasy VI 的 12 像素日文字。十六個 Kiradex 的 16 texel 圖塊是 256 texel,在世界本身的密度下容納 42 個拉丁字元與 21 個 PixelMplus12 的假名,或 64 與 32 個 Misaki 的字。11 而且以 Kiradex 自己的圖塊來量的文字框,在量測過的五個視圖中有三個放不下:16 個 16 texel 的圖塊,在每 texel 8 個裝置像素時是 683 點,比 iPhone 18 Pro Max(440 點)與直向的 Duo 內側顯示器(669)還寬;在 6 時是 512 點,比直向的 Duo 外側顯示器(466)還寬。它只放得進兩個橫向視圖:內側顯示器 951 點中的 512 點,以及外側顯示器 678 點中每 texel 4 個裝置像素時的 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 個字形的字型,會把它的檔案加進 App;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 對 commit 334fbfe 時磁碟上的 Kiradex(10月15日對現行原始碼樹重跑,先前的各行不變),measure_figure_width.py 對它的身體 sprite 表。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表;Core Text 在 8 與 12 像素下畫的是那些點陣圖還是輪廓,並沒有確定。7 - SwiftUI 自己的定位。 SwiftUI 是否把
Text的原點放在整數裝置像素上,以及當裝置語言為日文時它對日文套用哪一種斷行規則,都沒有量測。 - 字重。 PixelMplus10 Bold 是唯一量測過的粗體字體;任何像素字體上的合成粗體都沒有量測。7
- 易讀性。 網格與涵蓋範圍量測過了;但 10 乘 10 的漢字在 16.67 點下對日文讀者是否好讀,並沒有量測,需要一位日文讀者在裝置上判斷。
- 日文 ROM。 pret 建置的是英文版遊戲。日文版《綠寶石》本身的文字框、速度與字型都沒有讀過;第 1 節的日文字形表,是英文 ROM 為名字所帶的那些。日文版《水晶》與《紅》也沒有讀過。30
- Final Fantasy VI 的圖像。 反組譯專案只有範圍與程式碼,沒有像素;視窗圖塊的排列方式與漸層的樣子都沒有看過。英文大字型的範圍 5,120 位元組,不是 22 位元組字母的整數倍,原因不明。漸層表每條掃描線消耗一個值,是從註解得知,沒有追蹤驗證。4
- 《水晶》的閃爍週期 沒有推導出來;它的兩個狀態是從程式碼讀出的。16
- Chrono Trigger 完全沒有量測。我沒有複製到任何公開的反組譯專案,而 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 節的算繪全都不允許字型平滑化;在點陣圖環境的預設值下,只畫了一套字體的一個尺寸,PixelMplus10 在 10 像素,272 個著墨像素中有 178 個是部分像素。其他字體與尺寸沒有在開啟平滑化的情況下繪製。9
- 我閱讀時 App 正在改變。 10月5日撰寫本文時,Kiradex 正在被編輯。當天稍早,舞台的點擊處理被改寫,相關程式行位移了 30 到 144 行,世界的三個 Swift 檔案也還沒有進入任何 commit;在我閱讀的十四分鐘後,舞台與 rig 又改變了。本文閱讀的是 commit
334fbfe加上當天未提交的修改,以我閱讀時的狀態為準:App 的檔案在太平洋時間 12:28(伺服器的收藏者與城鎮地圖在 12:33),量測它的腳本在 12:34 與 12:36。註腳給出的是那次閱讀時每個檔案的行號;規格書以符號而不是行號來指稱,我在 12:42 確認過它指稱的每一個符號都還在。11 - App 執行的 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 節)。本文從系列其他文章中取用的三項事實,已經對照 App 檢查過:城鎮的 23 句預設台詞與 lines.py 相符;角色篇所說對話泡泡仍以格子高度決定位置,與 PlazaRig.headPoint 相符,它在走路者的位置上加上 sprite 表畫格高度的一半再加 4;而對話泡泡下方的人物,就是那篇文章的更新所描述的那一個,在 32 乘 40 的格子中高 30 texel,App 的清單檔、rig.py 與身體 sprite 表都證實了這一點。那篇文章只為它較早的 26 texel 人物給出寬度,肩寬約 14;第 12 節從 sprite 表量測目前人物的寬度。90100289396
未能取得的來源。 Data Crystal 的 Chrono Trigger ROM 對照表(HTTP 403);Pixel Operator 在 NotABug 上的儲存庫(HTTP 404);m5x7 與 m3x6 的字型檔(位於自訂價格的下載之後,沒有嘗試);以及 Celeste、Sea of Stars 或 Eastward 日文與中文文字的開發者來源(沒有找到)。49565758
12. 規格書:Kiradex 要做什麼,以及必須通過的檢查
App 此後已經往前推進。 到了10月15日的 commit fc9d4876,舞台的標籤與值改在一個 StageAccessibility 視圖修飾器中設定(PlazaStage.swift,套用於第 373 行,定義從第 520 行開始),仍然是「The plaza」,值仍是步伐軌跡,而當某個地點蓋住舞台時,這個元素就不存在。下面的檢查讀的是10月5日的版本,每一處「目前」指的都是那個版本;第 8 項同時適用於兩者。101
作者的綜合整理。 以下是我的提議,從第 1 到第 11 節推導而來,並以相對於2026年10月5日 Kiradex 狀態的差異寫成,也就是 commit 334fbfe 加上當天工作目錄中未提交的修改(第 11 節):每個世界視圖都用系統等寬文字、一行 caption 粗體的對話泡泡、底部的 WorldHUD 文字框、只有英文的 App 加上伺服器上的英文預設台詞、沒有字母的告示牌 sprite,以及一個被 VoiceOver 當作座標朗讀的舞台。2211 世界篇的方向不變:文字是美術;對話以 UI 尺度存在於 SwiftUI 中,不在世界的 texel 裡;以圖塊組成的文字框;兩行;打字機;續行標記。23 改變的是字體,理由見第 5 節,以及算術,因為 Dynamic Type 必須捨入到字型的網格。7 這裡沒有任何東西畫出某個系列作品的字形、外框、標記或詞彙;一切都由工坊繪製,或以授權條款已在第 4 節引用的字型排版。法律方面的說明是我的解讀,不是法律意見。這些都還沒有建置。
下面每一項檢查,都能由腳本、測試的日誌行或畫面擷取判定通過或失敗,唯一的例外是第 8 項中一項註明日期、在裝置上進行的人工檢查。若某項檢查的結果取決於裝置、文字大小、它挑選的那一行,或文字框同時顯示的其他內容,檢查會指明這些條件。規格書以符號而不是行號來指稱 App 的程式碼,因為我閱讀時程式碼正在移動;註腳給出的是我閱讀時的行號,附上 commit 與時間。若某一項背後的證據是在我 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 文字框、告示牌文字、任務看板與展示會的名牌。App 的其餘部分,也就是圖鑑、掃描器與設定,維持系統字體。
為什麼選它。 量測過的字體中,有兩套同時具備第 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
套件。 我的 App 共用的套件把每一種文字樣式都導向 .kit941Font(role),而它的自訂字型路徑會連續縮放字型,這會讓這套字體在 12 個 Body 尺寸中有 11 個偏離網格,也就是 Mac 上的 Core Text 讓它糊掉的那種情況。887 世界本身已經是一個宣告過的美術表面 WorldInk,有它自己的兩種顏色。25 新增一個 Kiradex 專屬的 PixelText 修飾器,以註解宣告為那個美術表面的例外,它接收一個套件角色並套用第 2 項的捨入;等它出貨之後,才向上游提議把它加為套件的一個字型家族選項。
不使用合成字重。 粗體文字使用 PixelMplus10-Bold;不在像素字體上使用 .weight(_:),因為 Core Text 對必須合成的字重會做什麼並沒有量測(第 11 節)。
檢查。
- 一項單元測試(Core Text 在 iOS 上也有)載入兩個內建檔案並斷言:每 em 1,000 單位;世界字串與預設台詞在兩種語言中所用的每一個字形,前進寬度都是 100 單位的倍數;樣本字形的輪廓點落在 100 單位網格的 4 單位以內(0.04 個字型像素,也就是第 4 節探測程式所用的容差)。未通過的字體不能出貨。
- 一項測試確認世界可能顯示的每一個字串(預設台詞、String Catalog 中世界相關的鍵、告示牌文字),都能對應到這套字體中的字形,不使用任何後備字型。
- 一支建置階段腳本在建置完成的 App 套件中找到 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 最小的 m₀ = 4(3× 下)是 13.33 點,高於 11 點的下限。1033 在 2× 下(iOS 27.0 模擬器為三款 iPhone 列出這個比例,見第 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 個字型像素,三乘三個 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 種中挑選選單的外框(它的對話框只保留一種外框),Final Fantasy 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 是我取名的測試參數,在 PlazaView 中以與 -plazaTell(它的 tellOnArrival)相同的方式讀取,並交給 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));這是否會讓內縮變窄,程式碼中沒有說明,所以 App 讀取它們,而不是假設它們。94
寬度規則。 當安全區域沒有側邊內縮時,每一句台詞在兩種語言中,都能在 App 可執行的每一款 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 App 只為 iPhone 建置,從 iOS 27.0 起,而我 Mac 上隨 Xcode 安裝的 iOS 27.0 模擬器執行環境列出 33 款 iPhone:3× 中最窄的寬 360 點(iPhone 12 mini 與 13 mini),有三款是 2×,即 375 點的第 2 代與第 3 代 iPhone SE,以及 414 點的 iPhone 11。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× 手機上是 2× 所允許的最小值 3;在直向的 Duo 外側顯示器上,沒有側邊內縮時是 6(434 中的 416),有量到的側軌時是 5(350 中的 349.33)。一句落在下文伺服器上限的日文台詞,21 個全形字元、210 個字型像素的前進寬度,會把 Duo 的上限降為 5 與 4,把 390 與 393 點手機的上限降為 4(列出的 iPhone 中有七款寬 390 點,四款寬 393 點),其他則維持不變。1197 App 從 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× 下,對照每個視圖的空間;由 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 個字元,所以「代號: 台詞」從 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
檢查。
- App 的字型測試(第 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),App 以 -worldTrace 啟動,每一次都由玩家說出該語言最長的台詞;在每一次擷取中,由一支腳本找到對話泡泡的墨色邊框,並斷言它高一行文字、寬不超過安全區域寬度減 32 點,並位於安全區域每一側再縮 16 點的範圍內。腳本從進行擷取的 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!」,並在現有 company 測試所允許的 5 秒內,等待對話泡泡 Said: Hi! 出現,這樣那句話就已經從房間傳回來,而依第 4 項的規則,它的分頁已經在文字框的佇列中,排在狀態之前,無論狀態有多長。城鎮的四位收藏者每位最多每 20 秒說一句,其中某一句可能排在它前面;佇列最多保留三句話,且絕不捨去玩家的那一句(第 3 項),所以最多有兩句排在前面,在那裡各最多兩頁,而之後說出的台詞排在它後面。測試在每次點擊前讀取 plaza-page,只有在該頁不是代號、冒號、空格加上「Hi!」時才點擊文字框,間隔一秒,最多四次,只要某次讀取找到那一頁就通過,最後一次讀取是在第四次點擊後一秒。它指定那句話,是因為對 names.py 能產生的每一個代號而言,「代號: Hi!」在那裡都只有一頁,而最長的台詞可能需要兩頁。它記錄它讀到的每一頁。11103102
5. 告示牌與名字
告示牌。 告示牌美術維持沒有字母,就像 kit.py 畫的那樣,讓一張圖集同時服務兩種語言。29 點擊告示牌圖塊,會以排在狀態之前的分頁(第 3 項)在 WorldHUD 文字框中打開告示牌的文字,而不是像現在這樣讓玩家走過去。目前舞台透過一個 DragGesture 讀取觸控:touched(_:in:) 讓玩家立刻走到手指下的地面,但在收藏者或販售亭上方時例外,那兩者會等手指抬起,而 lifted(_:in:) 把那次抬起讀作一次點擊。告示牌圖塊在這兩者中都加入收藏者與販售亭的行列:它等待抬起,而 lifted(_:in:) 會為它呼叫一個 onSign closure,就像它為販售亭呼叫 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 texel、側面時寬 13 texel 的人物上方(高 30,位於 32 乘 40 的格子中),在 18 Pro Max 上是 42.7 點或 34.7 點。119610033 代號顯示在點擊後打開的面板中,與現在相同,並由 VoiceOver 朗讀(第 8 項)。
檢查。 UI 測試無法知道告示牌的格子畫在螢幕上的哪裡,因為鏡頭會跟著玩家,所以測試以 -worldTrace 與 -plazaTapCell 7,10 啟動廣場,後者是我取名的測試參數,在 PlazaStage 的 body 中以與 -plazaFacingDemo 相同的方式(在一個 .task 中)讀取,它把格子送進同一套處理流程,而那套流程已從 lifted(_:in:) 抽出成一個方法。在 iPhone 18 Pro Max 模擬器上、以預設尺寸、不啟動廣場伺服器(所以沒有人說話),測試也傳入第 3 項的 -worldBoxSample,搭配一頁的狀態「Sample.」,並透過啟動參數中的設定鍵把文字速度設為 Instant(第 6 項)(這個鍵 world.textSpeed 是我取的,與搖桿的 world.stick 並列)。它最多等待 5 秒,讓 plaza-page 從「Sample.」變成告示牌的第一頁,然後讀取它,點擊文字框,再讀一次,告示牌文字的每一個單字(日文則是每一個字元)最多點擊一次,再加一次,直到 plaza-page 讀到「Sample.」為止,並確認在那之前的各頁依序連接起來,等於告示牌的英文文字(忽略空白),所以長到需要分頁的告示牌文字與短的同樣能通過;以日文作為 App 語言重新啟動後,則等於日文文字。92 工坊檢查會拒絕任何有 S 格子卻缺少任一語言的 town.json。
6. 文字速度與續行標記
內容。 一種以時間而不是影格計數的打字機式逐字顯示:shown = floor(elapsed × cps),一個以經過時間、速度與兩個設定(「減少動態效果」與 VoiceOver 是否執行中)為參數的純函式,讓 60 與 120 赫茲以相同的速度顯示。「設定」提供 Slow,每秒 12 個字元,即《水晶》的慢速;Normal,30,預設值,是我在經典作品的中速設定(《綠寶石》的 15、《水晶》的 20)與它的快速設定 60 之間做的選擇;以及 Instant。逐字顯示時點一下會讓這一頁顯示完畢;在已顯示完畢的頁面上點一下會翻頁。1 為此,文字框在逐字顯示中,或在目前這頁之後還有下一頁時接收點擊;在最後一頁完整顯示時,它讓點擊穿透到地面,就像它現在一直以來的做法。26 兩種語言使用相同的速度,就像 Final Fantasy VI 兩者都以一個影格一個字母印出。4 開啟「減少動態效果」,或 VoiceOver 執行中時,函式立刻傳回整頁;這是我的選擇,依據文件中開啟「減少動態效果」時「UI should avoid large animations」(UI 應避免大幅度的動畫)那一行。83
標記。 一個 Kiradex 自己、由工坊繪製的標記,一個 5 乘 3 個字型像素的向下楔形,位於文字框右下角,每 150 毫秒向下位移 0、1、2 與 1 個字型像素(方向是我的選擇;依我的解讀,《綠寶石》的是往上),一個循環 600 毫秒:也就是《綠寶石》在 60 赫茲下 9 個影格的一步。133
檢查。 一項以每秒 30 個字元測試逐字顯示函式的單元測試:t = 0 時 0 個字元,1.000 秒時 30 個,經過時間以 tick 計數計算,也就是 tick 數除以速率,在每秒 60 與 120 個模擬 tick 下進行,而不是把每個 tick 的差值加總(把 120 個 1/120 的差值以雙精度加總會得到 0.9999999999999989,乘以 30 是 29.999999999999968,向下取整為 29);同一個函式在開啟「減少動態效果」時,以及另外在 VoiceOver 執行中時,於 t = 0 傳回整頁;在 iPhone 18 Pro Max 模擬器上、以預設尺寸與 Slow、不啟動廣場伺服器的一項 UI 測試,以 -worldTrace 與第 3 項的 -worldBoxSample 狀態啟動,狀態的第一頁是兩行各 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. 日文
內容。 先為世界的文字做日文,App 的其餘部分之後再做。server/app/lines.py 保持它的索引順序,就像它的註解所說的「add at the end only」(只在最後新增),每一句台詞都變成一個英文字串與一個日文字串;伺服器兩者都送,而每個用戶端顯示它的使用者所讀的語言,所以在一個混合語言的廣場中說出的一句話,會從同一個索引以兩種語言被讀到。28 依我的解讀,這是這個 App 採用預設台詞最好的理由。世界字串,也就是 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 半形與全形字元區塊中,相容正規化(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 closure 的建置失敗訊息,以及來自讀取 -airProbe 之 .task 的空氣探測報告,幾分鐘後又多了第四個寫入,來自當天新增的一個走路示範。它移到一個只在 App 以 -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. 字體與視窗方面的絕不
- App 或工坊中絕不使用任何遊戲 ROM 或反編譯專案中的字形、外框、箭頭或視窗圖像。pret 與 ff6 的字形表在這裡只被量測,從不複製。Kiradex 的外框、標記與告示牌都由工坊繪製;它的字體是一套有授權的字型。
- 絕不使用系列作品的角色或連字。《綠寶石》的 charmap 為系列作品的詞彙帶有特殊字形;不畫任何類似的東西,也沒有任何字串使用系列作品對事物的稱呼。31
- 手上沒有授權檔的字型絕不使用:一套字體只有在其授權全文已在第 4 或第 5 節引用、並與證據一起保存時才能出貨。
- 絕不以保留字型名稱發布修改過的 OFL 字體。Press Start 2P 宣告了保留字型名稱;若任何 OFL 字體被修改,就必須改名。54
- 絕不把文字烘焙進世界美術:告示牌、店面與看板維持沒有字母,所以永遠不會有依語言區分的圖集。
檢查。 一支腳本為工坊寫出的每一張圖片與素材目錄中的每一張圖片計算兩個 SHA-256 摘要,一個是檔案位元組的,一個是解碼後像素的,並與這裡量測過的每一張 PNG(pret 的 graphics/fonts 與 graphics/text_window、《水晶》的外框,以及《水晶》與《紅》的 gfx/font/font.png)的同樣兩個摘要比對。提交進程式庫的是那些摘要的清單,而不是圖片本身,存為 scripts/forge/canon_digests.txt(我選的名稱,與工坊的 kit.py 並列),讓檢查不需要證據資料夾就能執行,而任何相符都會讓建置失敗。它自己的測試是一個不需要任何遊戲檔案的負向對照:把一個工坊輸出的摘要加進清單的一份副本,並斷言腳本因此失敗。同一支腳本會把工坊輸出中任何一個 1 位元遮罩(或其反相)與那些字形表中某一格相符的 8 乘 8 格子列出來供審閱,但不會讓建置失敗。整張圖片的摘要無法重建那些格子,所以清單也收錄字形表中每一個不同格子遮罩的 SHA-256 摘要,依同一條規則(我定的)產生,並以相同方式套用在工坊的輸出上:顏色與圖片左上角像素不同的像素即為設定;格子從左上角起以 8 乘 8 排列,邊緣不完整的格子捨去;全空或全實的格子在兩邊都略過,因為字形表裡有很多這種格子(光是 japanese_normal.png 的 1,024 格中就有 430 格全空);而一個遮罩就是它逐列排列的 64 個位元,8 個位元組。在 50 張字形表中,保留下來的格子共 7,746 個,不同的摘要共 1,484 個。99 重新上色、縮放或重畫的複本可以通過這些摘要比對;第 1 條規則仰賴的是工坊以程式碼繪製每一項素材,以及那份審閱,而不是只靠這項檢查。此外,本系列先前幾份規格書所提議的字串檢查,擴充加入《綠寶石》charmap 中系列作品的縮寫。105
不在這份規格書裡的項目
- 整個 App 都用像素字體。 圖鑑、掃描器與「設定」維持系統字體;像素字體是給世界用的。
- 中文與韓文。 檢查過的四款現代像素遊戲都有中文,其中兩款 Stardew Valley 與 Sea of Stars 也有韓文,但這裡沒有為兩者中任何一種量測過像素字型。64
- 以 Misaki 或任何 8 點字體排閱讀用的文字。 它是量測過最小的漢字;那個尺寸對日文讀者的易讀性尚未量測。6
- 頭頂的名字標籤 與 文字框中的姓名欄。1
- 沒有人審閱過的機器翻譯日文。
- 像素字體上的 合成粗體或斜體。
- 出售外框。 每一種外框都免費;支持者專屬的外框還在等待一個決定。
- 在 RealityKit 場景中繪製的文字。 世界篇的理由與第 10 節的算術都讓它留在 SwiftUI 中。2311
重點整理
如果您負責繪製美術
- 用圖塊組成外框:一個角、一條邊與一個中心。《綠寶石》的選單從 20 種 24 乘 24 像素、三乘三圖塊的外框中選擇;它的對話框由一條底邊鏡像的 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 的字型大約要畫七千個字;腳本固定的遊戲可以少一些,就像 Final Fantasy VI 的日文 ROM 只用了 1,248 個字母,但這樣一來每一個新字串都必須與字型比對。64584
常見問題
像素藝術遊戲的文字框應該多大?
在掌機版的 Pokémon 遊戲中,是螢幕下方三分之一,兩行。《綠寶石》的對話外框是 30 乘 6 圖塊,240 乘 48 像素,佔 Game Boy Advance 螢幕高度的 30%,框內是一個容納兩行 16 像素字型的 27 乘 4 圖塊文字視窗。《水晶》的文字框是 20 乘 6 圖塊,佔 Game Boy 螢幕的 33%,兩行各 18 個字元,《紅》的同樣是 20 乘 6。Final Fantasy VI 在螢幕頂部或底部開啟它的視窗。12345
打字機式文字效果應該多快?
經典作品以影格計算。《綠寶石》與《火紅》每 8、4 或 1 個影格印出一個字元,在 60 赫茲下每秒 7.5、15 或 60 個,而在印字過程中按過之後,按住 A 或 B 時一個影格一個;《水晶》與《紅》以每秒 12、20 或 60 個印出;Final Fantasy 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 上,擷取畫面才是定案的檢查。7
像素字型能支援 Dynamic Type 嗎?
只能透過捨入。在 iOS 十二個 Body 尺寸(14 到 53 點)中,10 像素的 em 在 3× 下只有 40 點落在整數裝置像素上,所以以 Font.custom(_:size:relativeTo:) 連續縮放的像素字型,在其他十一個尺寸都偏離網格;在 Mac 的 Core Text 上,偏離網格就代表糊掉,而 SwiftUI 在 iPhone 上會畫出什麼,要由在那裡的擷取來決定。以 UIFontMetrics 或 ScaledMetric 讀出縮放後的尺寸,捨入到每個字型像素整數個裝置像素,再以 Font.custom(_:fixedSize:) 設定。Apple 要求自訂字型實作系統字型所具備的行為,Dynamic Type 也在其中,並希望 App 最好能讓使用者把文字放大到至少 200%。710206768
Core Text 遵循日文斷行規則嗎?
在我於 macOS 27.0 上的量測中,未以語言標記文字時,是的,而且很嚴格:一段 131 個字元的段落,在 Hiragino Sans 與八套像素字體中,以七種寬度斷行,都沒有任何一行以探測程式行首清單中的任何字元開頭(這是一份不完整的清單,大多取自 JLREQ 的字元,其中七個是全形形式)。標記 ja 後,它放寬為 W3C 日文排版需求所描述的較寬鬆規則,在多數字體中,7 個寬度有 6 個讓小寫假名出現在行首。SwiftUI 在裝置語言為日文的 iPhone 上會怎麼做,沒有量測。721
日文像素字型需要多少個字元?
一般的涵蓋目標 JIS X 0208 有 6,879 個:524 個非漢字、2,965 個第一水準漢字與 3,390 個第二水準漢字。Final Fantasy Pixel Remaster 的製作人告訴 Siliconera,它的日文像素字型需要「around 7,500 characters」(約 7,500 個字元),而西方語言只需要「a few hundred」(幾百個)。腳本固定的遊戲可以少一些:Final Fantasy VI 的日文 ROM 在它的對話字型中有 1,248 個字母。584
本文中的每一個數字,都來自可對保存的來源重跑的腳本、2026年10月5日保存的頁面,或註明日期的 Kiradex 原始碼閱讀紀錄,而註腳會指明是哪一種;Core Text 的數字來自 macOS 27.0,不是 iOS。第 12 節的規格書是我的提議,也已如此標示;它的任何部分都還沒有建置。
本站相關文章:iPhone 上的像素藝術世界是本系列的第一篇,包含 RealityKit 的配方與本文所量測的文字方向;像素藝術人物:iPhone 上的角色與角色創建器是對話泡泡所在之處的那位收藏者;像素藝術建築:iPhone 上的房屋、大廳與室內有那個讓告示牌維持沒有字母的工坊;像素藝術的動態:iPhone 上的走路、鏡頭與門有逐字顯示所依據的 60 與 120 赫茲時鐘;沒有人在線上時也活著的像素城鎮有預設台詞與它們的速率;像素藝術房間與像素藝術路線是本文所延伸其檢查的規格書;向老 Pokémon 遊戲致敬而不借用它們說明了為什麼經典作品只被量測、從不被複製;而 Xcode 27.1 Beta:在 iPhone Duo 模擬器中執行您的 App 介紹了對話泡泡擷取所執行的模擬器。
資料來源
-
作者的量測,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的自動捲動延遲條件,以及對《水晶》常數的兩行解讀,都由腳本以斷言檢查,沒有實際執行。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/menu.c,commit731ad5b(sTextSpeedFrameDelays位於第 77 行;sStandardTextBox_WindowTemplates位於第 84 行;LoadMessageBoxGfx與LoadUserWindowBorderGfx位於第 212 與 213 行;WindowFunc_DrawDialogueFrame於第 218 行被呼叫,其填充從第 378 行起使用BG_TILE_V_FLIP),2026年10月5日讀取,https://github.com/pret/pokeemerald/blob/master/src/menu.c。 ↩↩↩↩↩↩↩ -
pret,
pokecrystal/constants/text_constants.asm,commit5beda23(第 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_WIDTH20 與SCREEN_HEIGHT18),2026年10月5日讀取,https://github.com/pret/pokecrystal/blob/master/constants/text_constants.asm。 ↩↩↩↩ -
作者的量測,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)。一個影格一個字母,以及漸層每條掃描線一個值,是對程式碼的解讀,以斷言檢查,沒有實際執行。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
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 個第二水準漢字。 ↩↩↩↩↩↩↩↩ -
作者的量測,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;它報告每套字型的像素尺寸、外框方塊、ascent 與 descent、字元數、前進寬度、墨跡在某個方向上填滿格子的全形字形、漢字(U+4E00 到 U+9FFF)的同一項資料,以及畫出的 JIS X 0208 字元。 ↩↩↩↩↩↩↩↩↩↩↩ -
作者的量測,2026年10月5日:本文作者證據資料夾中的
measure_coretext.swift,以swiftc -O編譯,在 macOS 27.0(build 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);它的設計網格(在每 em 4 到 40 像素之間,使樣本字形的輪廓點有 95% 落在像素 0.04 以內的最小每 em 像素數,每個軸各允許一個固定偏移);它的拉丁與假名前進寬度;它對 ASCII 與 JIS X 0208 的涵蓋範圍;把一行樣本文字以網格的 1 到 4 倍畫進 8 位元灰階點陣圖時,著墨像素中部分像素(255 中的 8 到 247)的比例,分別在開啟反鋸齒、關閉反鋸齒,以及開啟反鋸齒並開啟次像素定位、關閉量化的情況下,均不允許字型平滑化;PixelMplus12 另外在以偏移量移動基線後重做同樣的量測;《人機介面指南》中,字型像素在 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))。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
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」(約 7,500 個字元)對比「a few hundred」(幾百個)),2026年10月5日擷取,https://www.siliconera.com/interview-preparing-final-fantasy-pixel-remaster-for-the-switch-and-ps4/。 ↩↩↩↩↩↩
-
作者的量測,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 個。 ↩↩↩↩↩↩↩↩↩↩ -
Apple,《人機介面指南》,”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。 ↩↩↩↩↩↩↩
-
作者的量測,2026年10月5日:本文作者證據資料夾中的
measure_kiradex_text.py,輸出為measure_kiradex_text.out,於太平洋時間 12:34 針對 commit334fbfe加上當天未提交之修改的 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,分布在四個檔案中)、主題的字型家族、對話泡泡的修飾器、舞台的輔助使用修飾器、在 App 原始碼中搜尋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 句台詞所組成的「handle: line」,在五個視圖、十二個 Body 尺寸下的分頁(一頁兩行,於空格處貪婪斷行,依probe_line_start_glyphs.out,每一個可列印 ASCII 字元寬 5 個字型像素);measure_ios27_iphones.out中每一款 iPhone 的對話泡泡上限;伺服器的 218 字型像素上限;以及 120 次加上 1/120 的總和。當天更晚新增,先前的腳本與輸出保留在旁,輸出先前的每一行都不變:手機沒有側邊內縮時,以及直向的 Duo 外側顯示器上沒有內縮與有本系列 Duo 文章所量到的 84 點尾側內縮兩種情況下,安全寬度上的空間與上限,從那篇文章解析而來(安全寬度 382,空間 350,上限 5,349.33 點;預設尺寸下,文字框在外框下為 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日,最後一行依測試目前的樣子重新計算,在輸出所能顯示的範圍內使用相同的輸入(舞台檔案取自10月5日 12:28 讀取的複本;lines.py、names.py與npcs.py自10月2日與3日以來未變;其他輸入從 commitfc9d4876的現行原始碼樹讀取,印出的行相同),先前的腳本與輸出保留在旁;輸出的其他每一行都與先前相同。10月15日稍晚新增,先前的腳本與輸出保留在旁,先前的每一行都不變:擺放測試中目前最長的台詞在任何允許的m下都放不下的情況(有 84 點內縮時的 3× 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 種。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokered/data/text_boxes.asm,commitd2704a6,第 10 行(db MESSAGE_BOX, 0, 12, 19, 17),2026年10月5日讀取,https://github.com/pret/pokered/blob/master/data/text_boxes.asm。 ↩↩ -
pret,
pokeemerald/src/text.c,commit731ad5b(sFontInfos從第 119 行起;sDownArrowYCoords{ 0, 1, 2, 1 }位於第 75 行,sWindowVerticalScrollSpeeds位於第 76 行;AddTextPrinter於第 281 行設定textSpeed = speed,於第 296 行執行--sTempTextPrinter.textSpeed;;箭頭的downArrowDelay = 8位於第 832 行;在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 行的if (!(gBattleTypeFlags & BATTLE_TYPE_RECORDED) && gTextFlags.autoScroll),把計數器設為 3,否則設為textSpeed;EXT_CTRL_CODE_JPN位於第 1094 行;DecompressGlyph_Normal從第 1853 行起,GetGlyphWidth_Normal從第 1887 行起),2026年10月5日讀取,https://github.com/pret/pokeemerald/blob/master/src/text.c。 ↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokefirered/src/text.c,commit037335f,第 639 到 647 行(在RENDER_STATE_HANDLE_CHAR分支中,if (JOY_HELD(A_BUTTON | B_BUTTON) && subStruct->hasPrintBeenSpedUp)把計數器設為 0,而gTextFlags.canABSpeedUpPrint && JOY_NEW(A_BUTTON | B_BUTTON)在延遲倒數時設定hasPrintBeenSpedUp),2026年10月5日讀取,https://github.com/pret/pokefirered/blob/master/src/text.c。 ↩↩↩ -
pret,
pokecrystal/home/print_text.asm,commit5beda23(第 4 行,「The text speed setting in wOptions is actually a frame count」(wOptions 中的文字速度設定其實是一個影格數),以及按住 A 或 B 時PrintLetterDelay每個字母等待一個影格),與pokered/constants/ram_constants.asm,d2704a6(第 38 到 40 行:TEXT_DELAY_FAST1、TEXT_DELAY_MEDIUM3、TEXT_DELAY_SLOW5),2026年10月5日讀取,https://github.com/pret/pokecrystal/blob/master/home/print_text.asm。 ↩↩ -
pret,
pokecrystal/home/joypad.asm,commit5beda23(WaitPressAorB_BlinkCursor位於第 342 行;BlinkCursor位於第 442 行,把hlcoord 18, 17處的圖塊在'▼'與'─'之間切換),2026年10月5日讀取,https://github.com/pret/pokecrystal/blob/master/home/joypad.asm。閃爍的週期沒有推導出來。 ↩↩↩ -
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。 ↩↩↩↩↩
-
itouhiro,PixelMplus,
README.md(只有輪廓的 TrueType,「埋め込みビットマップは なし」(沒有內嵌點陣圖);「JIS第1・第2水準のすべての漢字を収録」(收錄 JIS 第 1、第 2 水準的所有漢字)與 ISO-8859-1;PixelMplus12 與 PixelMplus10;由 M+ BITMAP FONTS 建置;授權為 M+ FONT LICENSE)與misc/mplus_bitmap_fonts/LICENSE_E(M+ FONT LICENSE 的三句話),commitd89b95f,2026年10月5日讀取,https://github.com/itouhiro/PixelMplus/blob/master/misc/mplus_bitmap_fonts/LICENSE_E。 ↩↩↩↩↩ -
作者的量測,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。 ↩↩↩↩↩↩↩ -
Apple,《人機介面指南》,”Accessibility”(「Ideally, give people the option to enlarge text by at least 200 percent (or 140 percent in watchOS apps).」(理想情況下,讓使用者可以把文字放大到至少 200%,watchOS App 則為 140%。)),2026年10月5日擷取,https://developer.apple.com/design/human-interface-guidelines/accessibility。 ↩↩↩
-
W3C,”Requirements for Japanese Text Layout”,Working Group Note,2020年8月11日,第 3.1.7 節(行首禁則規則,以及關於較不嚴格規則的說明),2026年10月5日擷取,https://www.w3.org/TR/jlreq/。 ↩↩↩↩↩↩↩↩
-
作者為本文所做的研究檔案,2026年10月5日(未公開):上述量測、案例研究,以及為 Kiradex 撰寫的規格書初稿。 ↩↩↩
-
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」(請使用字級 16、32、48 等),以及關於《綠寶石》24 乘 24 文字視窗外框的那句話)、k = 8 與 k = 6 時文字的尺寸算術、引用 GoNintendo 提到約 7,500 個日文字元的 Pixel Remaster 段落,以及文字計畫(「m5x7 at integer multiples in a 24-by-24 nine-slice box, two lines, typewriter reveal, a two-frame advance arrow」(m5x7 以整數倍放進 24 乘 24 的九宮格框,兩行,打字機式逐字顯示,兩格的續行箭頭)),2026年10月5日讀取,https://blakecrosley.com/blog/pixel-art-world-on-iphone。這裡引用了那篇文章,但沒有修改它。 ↩↩↩↩↩↩↩↩↩↩
-
Kiradex 原始碼,
Kiradex/KiradexTheme.swift,第 12 行(let fontFamily: Kit941.FontFamily = .systemDesign(.monospaced)),於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫)。 ↩↩ -
Kiradex 原始碼,
Kiradex/World/PlazaStage.swift,於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫):第 146、155 與 189 行(stepTrace在RealityView的 make closure 中被設為建置失敗訊息、在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.」(玩家的最後一步,「x,y 朝向」,作為舞台的輔助使用值:走路測試會讀它,因為模擬器記錄的其他東西都傳不到它們那裡。))、第 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)"))。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Kiradex 原始碼,
Kiradex/World/WorldHUD.swift,於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫);該檔案日期為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 上xxs2、xs4、sm8、md16、lg24 點。 ↩↩↩↩↩↩↩ -
Kiradex 原始碼,
Kiradex.xcodeproj/project.pbxproj,第 1138 到 1141 行(knownRegions = (Base, en)),於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫);到 12:42 時該檔案已增加了幾行。 ↩↩ -
Kiradex 原始碼,
server/app/lines.py,於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫):docstring「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.」(索引是固定的;只在最後新增,App 依編號顯示它們。) ↩↩↩↩ -
Kiradex 原始碼,於2026年10月5日太平洋時間 12:28 讀取,位於 commit
334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫):scripts/forge/kit.py,第 529 與 530 行(def sign():,「A hanging sign on a bracket, 12 × 11.」(掛在支架上的告示牌,12 × 11。))、Kiradex/World/TileMap.swift,第 52 行(地圖字母"S": .sign),以及Kiradex/Resources/World/town.json(一個S格子,位於 x 7、第 10 列,於 12:33 以 Python 的json模組讀取)。 ↩↩↩↩ -
方法。pret 反編譯專案,以淺層複製讀取:pokeemerald
731ad5b(2026年10月1日)、pokefirered037335f(2026年9月26日)、pokecrystal5beda23(2026年9月29日)與 pokeredd2704a6(2026年9月22日),https://github.com/pret。Final Fantasy 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。 ↩↩↩↩↩↩ -
pret,
pokeemerald/src/fonts.c(gFontNormalLatinGlyphWidths從第 148 行起)與pokeemerald/charmap.txt(第 50 到 52 行,系列作品詞彙的特殊字形),commit731ad5b,2026年10月5日讀取,https://github.com/pret/pokeemerald/blob/master/src/fonts.c。 ↩↩ -
Martin Korth,GBATEK,”LCD Dimensions and Timings”(「Total 228 lines, 16.743 ms, 280896 cycles - ca. 59.737 Hz」(共 228 條掃描線、16.743 毫秒、280896 個週期,約 59.737 赫茲);160 條可見掃描線;每條可見掃描線 240 點),2026年10月5日擷取,https://problemkaputt.de/gbatek-lcd-dimensions-and-timings.htm。 ↩
-
作者依量測數字所做的算術: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;12 個 Body 尺寸中有 11 與 9 個偏離網格,由 1 與 3 個整數尺寸取補數而得;14 ÷ 17 = 82%,53 ÷ 17 = 312%;13.33 ÷ 16.67 = 80%,53.33 ÷ 16.67 = 320%;11 點 ÷ 每 texel 2 點 = 5.5 texel;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 texel × 2.67 點 = 42.7 點,13 × 2.67 = 34.7;9 個影格 ÷ 60 = 150 毫秒,四步 600 毫秒;3 點 × 3 = 掃描線之間 9 個裝置像素,每條 1 點 × 3 = 3 個裝置像素高。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩
-
pret,
pokeemerald/src/battle_message.c,commit731ad5b(第 3002 與 3003 行:當gBattleTypeFlags & (BATTLE_TYPE_LINK | BATTLE_TYPE_RECORDED)時開啟自動捲動;第 3010 行,連線對戰中訊息視窗的speed = 1;第 3012 行,錄影對戰的speed = sRecordedBattleTextSpeeds[GetTextSpeedInRecordedBattle()],表格{8, 4, 1, 0}位於第 1957 行)與pokeemerald/src/scrcmd.c(ScrCmd_messageautoscroll從第 1286 行起,在第 1292 與 1293 行設定autoScroll與forceMidTextSpeed),2026年10月5日讀取,https://github.com/pret/pokeemerald/blob/master/src/battle_message.c。 ↩↩↩ -
pret,
pokeemerald/charmap.txt,commit731ad5b,第 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。 ↩ -
pret,
pokeemerald/src/text_window.c(message_box.png位於第 53 行;LoadMessageBoxGfx位於第 93 行;LoadUserWindowBorderGfx位於第 110 行)、pokeemerald/include/text_window.h(第 4 行的#define WINDOW_FRAMES_COUNT 20)、pokeemerald/src/option_menu.c(FRAME 選項)與pokeemerald/graphics/text_window/,commit731ad5b,2026年10月5日讀取,https://github.com/pret/pokeemerald/blob/master/src/text_window.c。 ↩↩↩ -
pret,
pokeemerald/data/text/與每張地圖的scripts.inc,commit731ad5b,其中包括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。 ↩↩ -
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),commit731ad5b,2026年10月5日讀取,https://github.com/pret/pokeemerald/blob/master/src/international_string_util.c。 ↩↩ -
pret,
pokefirered/src/new_menu_helpers.c,commit037335f(sStandardTextBox_WindowTemplates從第 34 行起,第 40 行的.width = 26,以及文字速度延遲)與pokefirered/src/text_printer.c(AddTextPrinter,第 78 行的textSpeed = speed與第 93 行的--sTempTextPrinter.textSpeed;),commit037335f,2026年10月5日讀取,https://github.com/pret/pokefirered/blob/master/src/new_menu_helpers.c。 ↩ -
Pan Docs,”Rendering”(「One frame: 70224 dots @ 59.7 fps」(一個影格:70224 點,59.7 fps);一個影格「takes ~16.74 ms instead of ~16.67」(約需 16.74 毫秒,而不是約 16.67 毫秒)),gbdev.io,2026年10月5日擷取,https://gbdev.io/pandocs/Rendering.html。 ↩
-
pret,
pokecrystal/gfx/font.asm,commit5beda23(外框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。 ↩ -
everything8215,ff6,
tools/rip_list_en.json與tools/rip_list_jp.json,commit8130132(assets/gfx/window/window_%s.4bpp位於0xED0000到0xED1BFF;window_%s.pal位於0xED1C00到0xED1CFF),2026年10月5日讀取,https://github.com/everything8215/ff6/blob/master/tools/rip_list_en.json。 ↩ -
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」(訊息速度 = 3,戰鬥速度 = 3)),commit8130132,2026年10月5日讀取,https://github.com/everything8215/ff6/blob/master/src/menu/config.asm。 ↩↩↩ -
everything8215,ff6,commit
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。 ↩ -
everything8215,ff6,
src/field/hdma.asm(DlgWindowFixedColorTbl)與src/field/event.asm(第 1591 行,「dialog window y position (1 or 18)」(對話視窗的 y 位置,1 或 18)),commit8130132,2026年10月5日讀取,https://github.com/everything8215/ff6/blob/master/src/field/hdma.asm。 ↩↩↩ -
everything8215,ff6,
src/field/text.asm(LoadLetterGfx、從 x 4 到$e0的對話行、CalcTextWidth、NewLine、UpdateDlgTextOneLine、顯示 100 個影格的置中地圖標題)與src/gfx/font_gfx.asm(FontWidth),commit8130132,2026年10月5日讀取,https://github.com/everything8215/ff6/blob/master/src/field/text.asm。 ↩↩↩↩↩ -
everything8215,ff6,
tools/rip_list_jp.json(large_font.jp.1bpp位於0xC494C0到0xC4FFFF)與tools/char_table/,commit8130132,2026年10月5日讀取,https://github.com/everything8215/ff6/tree/master/tools/char_table。 ↩ -
everything8215,ff6,
src/field/reset.asm(原野迴圈:等待垂直空白期,每一輪呼叫UpdateDlgText一次),commit8130132,2026年10月5日讀取,https://github.com/everything8215/ff6/blob/master/src/field/reset.asm。這個反組譯專案從同一份原始碼建置英文與日文 ROM。 ↩↩ -
Data Crystal,”Chrono Trigger/ROM map”,datacrystal.tcrf.net,2026年10月5日擷取:HTTP 403,未讀取,https://datacrystal.tcrf.net/wiki/Chrono_Trigger/ROM_map。 ↩↩↩
-
Kiradex,
docs/TestFlight.md,第 5 步(第 192 到 194 行:在圖鑑中選擇日文時,系列與卡片以日文名稱顯示,下方附英文),於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫)。 ↩ -
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。 ↩↩↩↩
-
Num Kadoma,littlelimit.net,”8×12ドット日本語フォント「k8x12」”(8 乘 12 的格子、JIS 第一與第二水準、L 與 S 兩種變體、S 變體關於舊主機 8 乘 8 圖像的說明),2026年10月5日擷取,https://littlelimit.net/k8x12.htm。 ↩
-
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。 ↩↩↩↩ -
Google Fonts,
ofl/pressstart2p/OFL.txt(SIL Open Font License 1.1,「with Reserved Font Name “Press Start 2P”」(保留字型名稱為「Press Start 2P」))與METADATA.pb(設計者 CodeMan38),9710da1,2026年10月5日讀取,https://github.com/google/fonts/blob/main/ofl/pressstart2p/OFL.txt。 ↩↩↩ -
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。 ↩↩ -
DaFont,”Pixel Operator Font”(作者 Jayvee Enaguas;「Bitmap (16 px)」;15 個字型檔;241 個字形;作者的說明「Changed licence to Creative Commons Zero (CC0) 1.0」(授權改為 Creative Commons Zero (CC0) 1.0)與「licensed under a Creative Commons Zero (CC0) 1.0」(以 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。單一來源。 ↩↩↩↩
-
Daniel Linssen,”m5x7”,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」(素材授權 Creative Commons Zero v1.0 Universal);平均寬度 5、ascender 7、descender 2、x-height 5;它的字元被描述為「uppercase and lowercase letters, including many accented letters, numbers and most common symbols」(大寫與小寫字母,包括許多帶重音符號的字母、數字與大多數常用符號);自訂價格的下載),2026年10月5日擷取,https://managore.itch.io/m5x7。字型檔沒有下載。 ↩↩↩↩↩↩
-
Daniel Linssen,”m3x6”,itch.io(「use font size 16, 32, 48, etc」(請使用字級 16、32、48 等);「free to use with attribution」(署名即可免費使用);沒有素材授權欄位;自訂價格的下載),2026年10月5日擷取,https://managore.itch.io/m3x6。字型檔沒有下載。 ↩↩↩↩↩
-
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。 ↩
-
作者的量測,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)。 ↩↩↩↩↩ -
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.」(把 CJ 類字元當作 NS 類處理,會得到 CSS 的嚴格斷行;當作 ID 類處理,會得到 CSS 的一般斷行。)),2026年10月5日擷取,https://www.unicode.org/reports/tr14/。 ↩↩
-
Stardew Valley Wiki,”Modding:Dialogue”,最後編輯於2026年5月20日(
$b;頭像指令;Strings\SpeechBubbles),2026年10月5日擷取,https://stardewvalleywiki.com/Modding:Dialogue。 ↩↩↩ -
Stardew Valley Wiki,”Modding:Custom languages”,最後編輯於2026年1月3日(
UseLatinFont、.fnt檔、建議基準值為 1.5 的FontPixelZoom、FontApplyYOffset),2026年10月5日擷取,https://stardewvalleywiki.com/Modding:Custom_languages。 ↩↩ -
作者的量測,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日擷取。 ↩↩↩↩↩ -
作者的量測,2026年10月5日:本文作者證據資料夾中的
measure_apple_docs.py,輸出為measure_apple_docs.out:第 10 節引用的每一個保存的 Apple 文件頁面的標題、宣告、摘要、平台與開頭段落。 ↩↩ -
Apple,《人機介面指南》,”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。 ↩
-
Apple Developer Documentation,”UIFontMetrics”(「A utility object for obtaining custom fonts that scale to support Dynamic Type」(用來取得可縮放以支援 Dynamic Type 之自訂字型的工具物件);iOS 11.0)、”scaledFont(for:)”、”scaledFont(for:maximumPointSize:)” 與 “scaledValue(for:)”(「Scales an arbitrary layout value based on the current Dynamic Type settings」(依目前的 Dynamic Type 設定縮放任意版面數值);iOS 11.0),2026年10月5日擷取,https://developer.apple.com/documentation/uikit/uifontmetrics。 ↩↩↩
-
Apple Developer Documentation,”custom(:size:relativeTo:)”(一個「that scales relative to the given
textStyle」(相對於指定textStyle縮放)的自訂字型;iOS 14.0),https://developer.apple.com/documentation/swiftui/font/custom(:size:relativeto:),以及 “custom(:fixedSize:)”(一個「that does not scale with Dynamic Type」(不隨 Dynamic Type 縮放)的自訂字型;iOS 14.0),https://developer.apple.com/documentation/swiftui/font/custom(:fixedsize:),2026年10月5日擷取。 ↩↩↩↩↩ -
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。 ↩↩
-
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.」(限制 Dynamic Type 尺寸時,請考慮是否適合以accessibilityShowsLargeContentViewer()加入大型內容視圖。)),2026年10月5日擷取,https://developer.apple.com/documentation/swiftui/view/dynamictypesize(:)。 ↩↩ -
Apple Developer Documentation,”accessibilityShowsLargeContentViewer()”(iOS 15.0;「Don’t use the large content viewer as a replacement for proper Dynamic Type support.」(不要用大型內容檢視器取代適當的 Dynamic Type 支援。)),2026年10月5日擷取,https://developer.apple.com/documentation/swiftui/view/accessibilityshowslargecontentviewer()。 ↩↩
-
Apple Developer Documentation,”UIAppFonts”(「App-specific font files located in the bundle and that the system loads at runtime」(位於套件內、由系統在執行時載入的 App 專屬字型檔);iOS 3.2),2026年10月5日擷取,https://developer.apple.com/documentation/bundleresources/information-property-list/uiappfonts。 ↩↩
-
Apple Developer Documentation,”CTFontManagerRegisterFontsForURL(:::)”(iOS 4.1),2026年10月5日擷取,https://developer.apple.com/documentation/coretext/ctfontmanagerregisterfontsforurl(:::)。 ↩
-
Apple Developer Documentation,”interpolation(:)”(Image;iOS 13.0),2026年10月5日擷取,https://developer.apple.com/documentation/swiftui/image/interpolation(:)。 ↩
-
Apple Developer Documentation,”resizable(capInsets:resizingMode:)”(「Sets the mode by which SwiftUI resizes an image to fit its space」(設定 SwiftUI 縮放圖片以填滿空間的模式);iOS 13.0),2026年10月5日擷取,https://developer.apple.com/documentation/swiftui/image/resizable(capinsets:resizingmode:)。 ↩
-
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(:)。 ↩
-
Apple Developer Documentation,”typesettingLanguage(:isEnabled:)”(iOS 17.0;關於語言與裝置 UI 語言不符之文字的段落;「this language does not affect text localization.」(這個語言不影響文字的在地化。)),2026年10月5日擷取,https://developer.apple.com/documentation/swiftui/view/typesettinglanguage(:isenabled:)。 ↩↩
-
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。 ↩ -
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日擷取。 ↩↩↩ -
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.」(直接觸控區域會把觸控事件直接傳給 App,而不是交由 VoiceOver 等輔助技術處理。);該區域必須先被啟用),2026年10月5日擷取,https://developer.apple.com/documentation/swiftui/view/accessibilitydirecttouch(:options:)。 ↩↩
-
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。 ↩
-
Apple Developer Documentation,”AccessibilityNotification.Announcement”(iOS 17.0;
accessibilitySpeechAnnouncementPriority屬性),2026年10月5日擷取,https://developer.apple.com/documentation/accessibility/accessibilitynotification/announcement。 ↩↩ -
Apple Developer Documentation,”accessibilityReduceMotion”(iOS 13.0;「If this property’s value is true, UI should avoid large animations」(若這個屬性的值為 true,UI 應避免大幅度的動畫)),2026年10月5日擷取,https://developer.apple.com/documentation/swiftui/environmentvalues/accessibilityreducemotion。 ↩↩
-
Apple Developer Documentation,”AccessibilityComponent”(RealityKit;「A component that stores accessibility information for an entity」(為實體儲存輔助使用資訊的元件);「VoiceOver says aloud when it describes the entity」(VoiceOver 描述該實體時會唸出來)的名稱;iOS 17.0),2026年10月5日擷取,https://developer.apple.com/documentation/realitykit/accessibilitycomponent。 ↩↩↩↩
-
Apple Developer Documentation,”Improving the Accessibility of RealityKit Apps”(「set the properties on each visible
Entityin your scene」(在場景中每個可見的Entity上設定屬性)),2026年10月5日擷取,https://developer.apple.com/documentation/realitykit/improving-the-accessibility-of-realitykit-apps。 ↩ -
Apple Developer Documentation,”RealityView”(iOS 18.0),2026年10月5日擷取,https://developer.apple.com/documentation/realitykit/realityview。 ↩
-
Apple Developer Documentation,”XCUIVoiceOverService”(「Provides programmatic control of VoiceOver for UI testing」(為 UI 測試提供以程式控制 VoiceOver 的能力);iOS 27.0),2026年10月5日擷取,https://developer.apple.com/documentation/xcuiautomation/xcuivoiceoverservice。 ↩↩↩
-
941Kit 原始碼,
Sources/Kit941/Tokens/Typography/FontFamily.swift,commit50bc657(2026年9月28日),第 21 到 63 行(case custom(named: String),透過Font.custom(_:size:relativeTo:)算繪,display、title、body、label 與 caption 的固有尺寸為 34、22、17、13 與 12 點),2026年10月5日讀取;私人程式庫。 ↩↩ -
Kiradex 原始碼,
server/app/names.py(第 7 到 14 行,FIRSTS與SECONDS,各 20 個字,以及由兩個首字母大寫的字與四位數字組成的代號格式),於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫)。 ↩↩ -
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。 ↩↩
-
Kiradex,
docs/WORLD.md,第 5 節「The stack」,第 524 與 525 行(速率限制:「eight moves a second, a line every two seconds」(每秒八次移動,每兩秒一句話)),於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫)。 ↩↩ -
Kiradex 原始碼,於2026年10月5日太平洋時間 12:28 讀取,位於 commit
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 行,走路測試在那裡以舞台框架的分數位置點擊舞台。 ↩↩↩↩ -
Kiradex 原始碼,
Kiradex/World/PlazaRig.swift,於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫):第 432 行的fit(viewSize:displayScale:tilesTall:fitsWhole:)(pixelScale為視圖像素高度除以tilesTall × TileMap.tileSize,即 20 × 16 = 320,後取下整數,第 439 行;TileMap.tileSize在TileMap.swift第 60 行為 16),以及第 479 行的headPoint(of:)(走路者的位置加上 sprite 表畫格高度的一半,再加 4,第 482 與 483 行)。該檔案到 12:42 時又改變了;那時兩個函式都沒有改變。 ↩↩ -
Kiradex 原始碼,於2026年10月5日太平洋時間 12:28 讀取,位於 commit
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」(位於它的舞台之上、安全區域之內,在 Duo 上這讓它避開攝影機與側軌));以及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」(卡片停在安全區域內,避開 Duo 的外側攝影機與側軌),同樣的兩個修飾器位於第 56 與 57 行。在Kiradex/中搜尋safeAreaInsets,只找到CardViewer那一行。 ↩↩↩↩↩ -
Blake Crosley,”Xcode 27.1 Beta: Your App in the iPhone Duo Simulator”,blakecrosley.com,2026年9月21日,2026年10月2日更新(在 iPhone Duo 模擬器的外側與內側顯示器上執行 App;闔上時、以 27.1 SDK 連結,一個 466 乘 678 點的視窗,其位於導覽堆疊內的內容量得 382 乘 562 點,安全區域內縮為頂部 82、底部 34、尾側 84(側軌),探測中該側有工具列與標籤列,並顯示狀態列),https://blakecrosley.com/blog/xcode-27-1-beta-iphone-duo-simulator。 ↩↩↩↩
-
作者的量測,2026年10月5日:本文作者證據資料夾中的
measure_figure_width.py,輸出為measure_figure_width.out,於太平洋時間 12:36 針對 commit334fbfe加上當天未提交之修改的 Kiradex 重跑,輸出不變。輸入,只從 Kiradex 讀取:Kiradex/Resources/World/characters.json("cell": [32, 40]、畫格與朝向)、scripts/forge/rig.py(第 53 行,CELL_W, CELL_H = 32, 40)與身體 sprite 表fig-body.png(1,376 乘 160 像素,43 個畫格乘 4 個朝向)。在站立畫格idle_a中,人物在每個朝向都高 30 texel,面向下或上時最寬的一列寬 16,面向左或右時寬 13。 ↩↩↩ -
作者的量測,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 的相容性頁面,後者沒有擷取。另外從 commit334fbfe的 Kiradex 專案檔唯讀取得:App target 在它的兩種設定中都是TARGETED_DEVICE_FAMILY = 1(iPhone)(12:28 讀取時的第 1559 與 1705 行),以及專案兩種設定中的IPHONEOS_DEPLOYMENT_TARGET = 27.0(第 1608 與 1670 行),App target 沒有覆寫它。 ↩↩↩↩↩↩↩ -
作者的量測,2026年10月5日:本文作者證據資料夾中的
probe_line_start_glyphs.swift,以swiftc -O編譯,在 macOS 27.0(build 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 個,兩者不相同,部分像素都是 0。每一個可列印 ASCII 字元的前進寬度都是 5 個字型像素。10月5日稍晚擴充,先前的腳本與輸出保留在旁,輸出先前的每一行都不變:半形與全形字元區塊(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 計算的是項數)。 ↩↩↩↩ -
作者的量測,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 個不同的摘要。 ↩↩ -
Blake Crosley,”Pixel-Art People: Characters and a Creator on iPhone”,blakecrosley.com,2026年10月3日,2026年10月5日讀取:關於 build 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」(格子是 32 乘 32;人物到頭頂高 26,大多數髮型為 27(髮髻、刺蓬或蓬髮為 28),肩寬約 14);在 build 34 的更新中,「The cell is 32 by 40 now, with eight rows of headroom and two under the feet, and the figure 30 pixels」(格子現在是 32 乘 40,頭頂留八列空間,腳下兩列,人物 30 像素);以及清單檔中給對話泡泡與 3D 卡片用的頭部與手部錨點,「though both still take their place from the cell’s height for now」(不過兩者目前仍以格子的高度決定位置),https://blakecrosley.com/blog/pixel-art-characters-on-iphone。 ↩↩
-
作者對 commit
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日的,並已註明日期。 ↩ -
Kiradex 原始碼,
server/app/npcs.py,於2026年10月5日太平洋時間 12:33 讀取,位於 commit334fbfe(該檔案日期為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 讀取。 ↩↩ -
Kiradex 原始碼,
KiradexUITests/KiradexUITests.swift,於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫);該檔案日期為 10:32,讀取期間沒有改變:第 228 行(Me 分頁的me-handle保存代號,兩個字加一個數字)、從第 432 行起的testTheCollectorFacesTheWayItWalks(針對每個朝向讀取舞台的值)、從第 502 行起的testThePlazaKeepsTime(讀取plaza-status並檢查其前綴,第 509 行)、從第 523 行起的testTheChimneysSmoke(以-airProbe啟動,等待舞台的value CONTAINS 'smoke 18 ',第 531 行)、等待room-status開頭幾個字的testVisitingARoomIsAVisit(第 469 到 472 行)與testYourRoomHasYourBinders(第 604 到 607 行)、等待hall-status的testTheShowHallHasEveryonesCards(第 490 到 494 行)、等待plaza-status中「here」人數達五人以上的testTheTownHasCollectors(第 672 到 676 行),以及從第 689 行起的testThePlazaHasCompany(除非KIRADEX_PLAZA=1也就是有伺服器,否則略過,而依它的略過訊息,它也需要另一位玩家,第 690 與 691 行;等待plaza-status的人數,從「Say」選單選擇「Hi!」,並等待 5 秒讓對話泡泡Said: Hi!出現,第 714 行)。 ↩↩↩↩ -
Kiradex 原始碼,
server/requirements.txt(fastapi、uvicorn[standard]、websockets、httpx與pytest,沒有字型函式庫)與server/tests/(test_rooms.py),於2026年10月5日太平洋時間 12:28 讀取,位於 commit334fbfe加上當天未提交的修改,取自一個當天正在被編輯的工作目錄(私人程式庫)。 ↩ -
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。 ↩