← 所有文章

無人上線也熱鬧的像素小鎮:NPC 與幽靈

像素小鎮之所以讓人覺得有人居住,靠的大多是站著不動的人。在《寶可夢 綠寶石》(Pokémon Emerald)的 16 座城鎮中,沒有隱藏旗標、因此每次造訪都在場的 100 位鎮民分成三類:59 位站在定點,面向固定方向或四處張望;39 位在離安置點一到兩格的範圍框內遊走;2 位原地踏步。再加上 58 位可被劇情旗標移除的人物(多半是反派組織成員、勁敵與有名字的角色,其中 54 位站著),比例就變成 158 位中的 113 位(71.5%)、43 位(27.2%)與 2 位。遊走者走一步花 16 個影格,接著等待 32、64、96 或 128 個影格,所以處於移動中的時間最多只占 11% 到 33%。1 一天的帳在玩家回來時才結算:由一個函式帶著經過的天數執行一次,而不是讓時鐘在無人遊玩時持續運轉。23 沒有其他人在場時,經典作品會用其他人留下的痕跡填滿城鎮,放進少量固定的欄位(《綠寶石》的紀錄混合會把 20 座秘密基地、5 句流行語,以及另一位玩家的老人複製到您的遊戲裡),並讓陌生人共享在場感而不共享言語。45 我之所以量測這一切,是因為我自己的小鎮 Kiradex World 在第一個安靜的早晨也得回答同一個問題,而它的伺服器自身的程式碼,在模擬時鐘上驅動之後,有三處答得不好:房間裡沒人時,四位收藏者就停住;它們每秒才走一步,設定值卻是 0.7 秒,每段步行有 75% 的時間站著不動;此外,在一個依 app 自身繪製程式碼建立的模型中,移動訊息若在一步走到一半時抵達,另一位收藏者會被畫成往後跳:跑步時 64 次移動中有 45 次如此,40 個圖塊的步行在 30 與 80 毫秒的網路抖動下分別有 7 與 16 個影格如此,不過在沒有抖動的穩定步行中從未發生。67 本文依序呈現量測過的經典、現代的參考作品、即時在場及其數字、兒童可以進入的世界所需遵守的規則,以及規格書。

重點摘要

  • 小鎮裡大多數人是站著的。 《綠寶石》城鎮中沒有隱藏旗標的 100 人裡,有六成的預設移動類型是靜止的;把劇情可移除的 58 人也算進去,158 人中就是七成(劇情腳本仍可讓站著的人走動,例如橙華市道館前的男孩會帶玩家走到道館)。其餘幾乎都在一到兩格的範圍框內,以與玩家相同的步調遊走,每格 16 個影格(268 毫秒),每步之間等待 0.54 到 2.14 秒。未白鎮有 6 人、橙華市 7 人、紫堇市 10 人,其中分別有 2、3、6 人沒有隱藏旗標。18
  • 一份作息是幾個定時點加上一把鍵;一份記憶是每天一個旗標。 《星露谷物語》(Stardew Valley)把村民的一天寫成一串定時點(Pierre 的平日有 7 個),並從分成三組的 22 種鍵格式中挑出當天適用的那一份,先符合者勝出:先為所有人檢查一個特殊鍵,接著已婚村民只看 5 種婚姻格式、不再看其他格式,其他所有人則看 16 種一般格式;每天與村民交談一次可得 +20 好感度。《集合啦!動物森友會》(Animal Crossing: New Horizons)則是每天第一次交談給 255 分中的 +1 分,並且每天教一個反應動作。29101112
  • 其他人的痕跡會填滿空蕩的小鎮,放在固定的欄位裡,而且會衰退。 《綠寶石》的紀錄混合攜帶另一位玩家世界的 5,188 個位元組;在交流大道(Join Avenue),粉絲會以訪客價值的 75% 補上真正訪客留下的空店位(依 Serebii 的說法);Death Stranding 的建築會「destroyed by Timefall after some time」(在一段時間後被時間雨摧毀);Journey 一次只讓您遇見一位陌生人,直到片尾字幕前都看不到名字,彼此之間只有一種鳴響聲。413145
  • 即時在場是一個緩衝,不是一個傳送頻率。 由粉絲打造的 Habbo 伺服器模擬器以 500 毫秒為週期推進每個房間;Gaffer on Games 的緩衝是「3X the packet send rate」(封包傳送間隔的 3 倍);在一個以本系列每秒 3.75 與 7.5 格的速度模擬 Kiradex 步行的模型中,以比估計伺服器時間晚 350 毫秒的時鐘來繪製其他收藏者,在最多 80 毫秒的抖動下,往後跳的影格與斷糧的影格都是 0;若只晚 100 毫秒,傳送端持續傳送期間所繪製的 600 個影格中,有 125 到 456 個斷糧。15167
  • 兒童相關的規則都附有日期。 Game Center 把兒童限制在「to sending and receiving preset messages」(只能傳送與接收預設訊息);Apple 的文件說,當 Screen Time 限制多人遊戲時,具有自訂多人功能的遊戲「should disable it」(應將其停用);修訂後的 COPPA 規則自 2026年4月22日起必須遵守,要求一份載明刪除期限的書面資料保留政策。171819
  • 今天的 Kiradex 有四處與它自己的計畫不同。 房間沒人時,廣場上的收藏者就停住,儘管 server/app/npcs.py 寫著「A town is never empty」(小鎮從不空蕩);它們每 1.0 秒走一步,設定值卻是 0.7 秒;app 每走一個圖塊送出一次移動(步行時每秒 4 次,跑步時每秒 6.0 到 6.4 次),而計畫中的推論寫的是 8 到 10 赫茲;而且沒有任何程式讀取 Game Center 的限制旗標,儘管計畫把身分與家長控制都建立在 Game Center 上。62021222324
  • 這帶給 Kiradex 的是什麼。 一份規格書:八位鎮民,作息以 app 自己的時段為鍵,每個房間一個時鐘,位置由那個時鐘計算出來;最近八位訪客的步行以無名的殘影重播,穿著伺服器挑選的樸素造型,保留七天,傳送時不帶任何 id;一句每天一次、永不衰退的問候;350 毫秒的算繪緩衝;一本分六個分支、每支 8 到 12 句完整句子的台詞簿;以及七項安全設定,每一項都附有一個可由腳本、測試或擷取畫面判定通過與否的檢查。7119

1. 實測鎮民:《綠寶石》、《星露谷物語》與《動物森友會》

每個小型社交世界在第一晚都會碰上同一個問題:玩家走進廣場,卻沒有其他人在,他會看到什麼?經典作品給出三種答案,本文依序討論。第一種是過著自己生活的鎮民,不論有沒有人上線,小鎮都很熱鬧。第二種是其他人留下的痕跡,來自此刻不在場的玩家。第三種是在有人時把即時在場做好,並以兒童可進入的世界所需的規則為界。第一種答案歷史最久,也量測得最清楚,因為其中一部名作的原始碼是公開的。

《綠寶石》裡的人在做什麼

《寶可夢 綠寶石》是透過 pret 的反編譯來讀的,使用 pokeemerald 在 commit 731ad5b 的淺層複製,並由一支腳本 measure_npcs.py 計數,它讀取移動類型常數,以及每一張城鎮地圖的物件事件。1 《綠寶石》定義了 81 種移動類型,從 0x00 到 0x50。依畫面上的人物在做什麼來分組:24 種沿四段固定迴圈行走,17 種站立並面向某處(固定、交替或四處張望),16 種原地走、慢跑或奔跑,8 種模仿玩家,5 種在範圍內隨機遊走,4 種來回踱步,4 種隱藏或偽裝,最後三種是 NONE、玩家本身,以及樹果樹,而樹果樹是物件而不是人。125

城鎮只用到其中少數幾種。16 張城鎮地圖上共有 172 個物件事件,但它們並不全是人:6 個是道具球,2 個是搬家卡車,1 個是船,5 個是寶可夢。腳本依 graphics_id 將它們分類,單獨計算其中的 158 個人。18 81 種移動類型中,只有 11 種出現在這些人身上。158 人中有 113 人(71.5%)站立並面向某處,43 人(27.2%)在範圍內遊走,2 人(1.3%)原地踏步。最常見的單一類型是 FACE_DOWN 32 人、FACE_RIGHT 23 人、LOOK_AROUND 20 人、FACE_UP 19 人,以及 WANDER_AROUND 17 人。18

這 158 人並不全是居民。其中 58 人帶有隱藏旗標,也就是會讓他們從地圖上消失的劇情旗標,而這 58 人中有 41 人使用反派組織、勁敵或有名字角色的 sprite,所以這 58 人多半是劇情場景中的演員;其中 54 人是站著的。沒有隱藏旗標、每次造訪都在地圖上的 100 人,分布則不一樣:59 人站立並面向某處,39 人在範圍內遊走,2 人原地踏步。所以玩家大部分時間看到的居民,是六成站著、將近四成遊走;整體的七成,是加上那些幾乎都站著的演員後才得出的。這些是預設移動類型,也就是沒有腳本驅動時人物的行為;劇情腳本仍然可以讓站著的人走動,例如橙華市道館前的男孩,預設是四處張望、沒有隱藏旗標,卻會帶玩家走到道館。這份計數描述的是靜止狀態的小鎮,而不是小鎮裡的每一個場景。18

一張堆疊長條圖,依移動類型呈現《綠寶石》城鎮物件事件中的人物。全部 16 座城鎮,158 人:113 人站立並面向某處(71.5%),43 人在範圍框內遊走(27.2%),2 人原地踏步。沒有隱藏旗標的 100 人:59 人站立(59%),39 人遊走(39%),2 人原地踏步。未白鎮 6 人:3 人站立、3 人遊走。橙華市 7 人:5 人站立、2 人遊走。紫堇市 10 人:8 人站立、2 人遊走。

在《綠寶石》地圖上始終存在的鎮民中,六成的預設移動類型是靜止的;把劇情中可移除的演員算進去則是七成;遊走者只在一到兩格的範圍框內活動。可以讓任何人走動的腳本場景不計入。26

三座大小不同的城鎮呈現同樣的樣貌。下表只計算人物;被排除的物件事件是未白鎮的兩輛卡車,以及三個道具球(橙華市兩個、紫堇市一個)。最後一欄依行為計算沒有隱藏旗標的人。1

城鎮 人數 站立並面向某處 在範圍框內遊走 遊走範圍 (x, y) 有隱藏旗標 無隱藏旗標(站立,遊走)
未白鎮 6 3(面向上) 3(WANDER_AROUND) (1, 2), (2, 1), (2, 1) 4 2(0,2)
橙華市 7 5(2 四處張望、2 面向上、1 面向下) 2(1 四處、1 上下) (1, 1), (0, 1) 4 3(2,1)
紫堇市 10 8(2 四處張望、2 面向上、2 面向左、1 面向右、1 面向下) 2(左右) (1, 1), (1, 0) 4 6(4,2)

遊走者的範圍框是一格一格強制執行的:每走一步之前,IsCoordOutsideObjectEventMovementRange 會拒絕任何超出該物件從安置點起算之範圍的目標,所以遊走者是繞著自己的原點打轉,而不是四處旅行。27 隱藏旗標則是人口的另一半。未白鎮 6 人中有 4 人、橙華市 7 人中有 4 人、紫堇市 10 人中有 4 人,帶有會在劇情推進時讓他們消失的旗標,所以小鎮的人口是玩家進度的函數:人會離開、會到來,是因為發生了某件事。1

他們多久動一次

時間設定在同一個檔案裡。遊走或四處張望的鎮民,在下一步或下一次轉身之前,會從 sMovementDelaysMedium 隨機挑出 32、64、96 或 128 個影格來等待:以 Game Boy Advance 的 59.7275 赫茲計算,是 0.54、1.07、1.61 或 2.14 秒,平均 1.34 秒。在兩個相反方向之間交替的兩種面向類型 FACE_DOWN_AND_UP 與 FACE_LEFT_AND_RIGHT 也一樣。在兩個相鄰方向或三個方向之間交替的面向類型,則從 sMovementDelaysShort 挑選 32、48、64 或 80 個影格,平均 0.94 秒;兩種旋轉類型則固定等待 48 個影格。第三張表 sMovementDelaysLong 在原始碼中標著 // Unused,沒有任何移動類型讀取它。127

一步本身與玩家的相同。PlayerWalkNormal 呼叫 GetWalkNormalMovementAction,遊走者的一步用的也是這個動作;MOVE_SPEED_NORMAL 對應到 sStep1Funcs,每影格移動 1 像素、共 16 個影格,也就是 268 毫秒走完一個 16 像素的格,每秒 3.73 格。跑步是 MOVE_SPEED_FAST_1,sStep2Funcs,每格 8 個影格、134 毫秒,每秒 7.47 格。127 把這兩列放在一起,遊走者在每「一步加一次等待」的 0.81 到 2.41 秒中,有 268 毫秒在移動:以每走一步計,移動時間占 11% 到 33%。這是上限。遊走者挑的方向若被擋住,不論是牆壁還是範圍框的邊緣,MovementType_WanderAround_Step4 會讓它不移動、重新轉身再等待,所以它實際處於移動中的比例更低。其餘時間它站著,或轉身。127

一張六秒的時間軸圖。四列是《綠寶石》的遊走者,每列一種等待長度:等待 32 個影格時,移動時間占 33%;64 個影格 20%;96 個影格 14%;128 個影格 11%。第五列是 Kiradex 的收藏者在一段步行中,每 1.0 秒移動 250 毫秒,占 25%。

《綠寶石》的遊走者在漫長的等待之間一次只走一步;Kiradex 的收藏者(第 6 節量測)在理應步行的時候,卻是一次衝一個圖塊的距離。26

依我的解讀,有三個細節造就了這種效果。小鎮裡沒有任何東西走得比您快或慢,所以沒有什麼會被看成另一類東西。停頓比步伐長,所以小鎮是一連串靜止畫面中偶爾穿插動作,這也是為什麼 6 到 10 個人在裡面也從不顯得擁擠。人口透過隱藏旗標寫進了劇情,所以小鎮的改變是因為玩家做了某件事,而不是因為某個計時器觸發了。

《綠寶石》的一天在抵達時結算

《綠寶石》有「一天」的概念,但不是靠一個在無人遊玩時持續運轉的時鐘。它在 DAILY_FLAGS_START 之後保留 64 個每日旗標欄位,用了其中 12 個:九個樹果贈送(一個華麗大賽大廳的、樹果大師與他太太的、五個道路或城鎮贈送者的、花店的)、秘密基地的每日旗標、彩券,以及 FLAG_DAILY_APPRENTICE_LEAVES。228 玩家回來時,UpdatePerDay 會帶著經過的天數執行一次,遊玩期間也會由 DoTimeBasedEvents 呼叫;它依序呼叫 ClearDailyFlags、UpdateDewfordTrendPerDay、UpdateTVShowsPerDay、UpdateWeatherPerDay、UpdatePartyPokerusTime、UpdateMirageRnd、UpdateBirchState、UpdateFrontierManiac、UpdateFrontierGambler、SetShoalItemFlag 與 SetRandomLotteryNumber。23 流行語、電視節目與天氣,全都依經過的天數推進,在回來的那一刻計算。3 卡匣關機期間什麼都沒發生;小鎮卻表現得彷彿發生過一樣。

《星露谷物語》:一份作息就是一個字串

說到有自己生活的鎮民,《星露谷物語》是現代的參考作品。它的 wiki 的「Villagers」頁面列出 6 位單身漢、6 位單身女子、22 位不能結婚的村民,以及 12 位不能送禮的村民,所以可以送禮的村民有 34 位;頁面寫道:「Each villager has a daily routine, so they can be located in different sections of town depending on the in-game time of the day and weather.」(每位村民都有日常作息,因此依遊戲內的時刻與天氣,他們會出現在鎮上不同的區域。)229

作息是資料。每位村民在 Content/Characters/schedules/ 底下有一個檔案,每一筆是一個以斜線分隔各點的字串,每一點是 <time> [location] <tileX> <tileY> [facing] [animation] [dialogue],時間採 24 小時制、不加冒號,面向以 0 表示上、1 右、2 下、3 左。9 頁面給出的 Abigail 週三作息是 1000 ArchaeologyHouse 11 9 0/1800 Town 47 87 0/2200 SeedShop 1 9 3 abigail_sleep:三個點,每兩點之間走一段路,最後一點播放一段動畫。9 經營雜貨店的 Pierre 在平日有 7 個定時點(早上 6:00 在櫃檯,7:00 在貨架間,8:30 回到櫃檯,接著是下午 5:00、晚上 7:00、9:00 與 11:00,依序走過貨架、廚房、書櫃與床),他的頁面列出 6 種作息變化:綠雨、春季 15 日修好的公車、沙漠節、雨天、週五與平日。230

聰明之處在於鍵。某一天執行哪一份作息,由 22 種鍵格式決定,分成三組,每組依固定順序嘗試,先符合者勝出。一個特殊鍵 GreenRain「checked first, regardless of marriage status」(最先檢查,不論婚姻狀態)。已婚村民接著只嘗試 5 種婚姻格式,從 marriage_<festivalID>、日期、marriageJob 到星期幾:「Married NPCs don’t use any other schedule keys. If the marriage keys don’t match, they won’t have a schedule for that day.」(已婚 NPC 不使用任何其他作息鍵;婚姻鍵若都不符合,他們當天就沒有作息。)其他村民則嘗試 16 種一般格式,從 <festivalID>、<season>_<dayofmonth>、<dayofmonth>_<hearts>、<dayofmonth>、bus、rain2(「50% chance of applying on rainy days」,雨天有 50% 的機率套用)與 rain,接著是星期幾加愛心數的鍵與星期幾,再到 <season>、spring,最後是 default。29 所以依我的解讀,一位村民有許多種可能的日子,同樣幾個點重新組合成一段生活,而愛心鍵代表喜歡您的村民會過不一樣的一天。頁面自己關於限制的說明,透露了一天是怎麼算出來的:NPC 若被加入既有的存檔,「they generally don’t follow their schedule correctly until you’ve slept once in-game (which triggers their first day update).」(通常要等您在遊戲中睡過一次覺,觸發他們的第一次每日更新後,才會正確依照作息行動。)9 與《綠寶石》一樣,《星露谷物語》的一天是在日子的交界處計算出來的,而不是連續推進的。

好感度是每天的記憶,而且附帶代價。一顆愛心是 250 點;「talking to them once per day (+20)」(每天與他們交談一次,+20)是 wiki 列出的提升方式中的第一項;村民每天收一份禮、每週收兩份,生日禮物乘以 8;不交談則每天扣點,多數村民扣 2 點,送過花束後扣 10 點,直到好感度滿格或達到上限為止;配偶則扣 20 點,而且衰退永遠不會停。10 愛心事件,也就是腳本場景,會在愛心門檻開啟,而且「most events can be viewed anytime (with some time restrictions) or out of order.」(大多數事件可以隨時觀看,有些有時間限制,也可以不照順序觀看。)10 村民也會在您不在時對您做些事:3 顆愛心時,Pierre「will send you a recipe in the mail」(會寄食譜給您),在任何大於零的等級,他都可能寄來 250g 或更多。30

《動物森友會》:知道您來過的村民

《集合啦!動物森友會》把「知道您今天跟他說過話的村民」濃縮成單單一點。好感度從 0 到 255,起始值是 25;Nookipedia 列出提升好感度的方式,第一項是「speaking to the villager for the first time each day」(每天第一次與村民交談)可得「+1 point」(+1 點);村民每天收一份禮;好感度達 150 點時,村民有 6% 的機率在收禮時回送照片,之後每多 1 點增加 0.04%,到 255 點時是 10.2%。11 依我的解讀,它是一個旗標而不是一個量表,因為玩家從來不會看到它變動。

村民看得見的生活不多,但屬於他們自己。每位村民有六種嗜好之一(教育、時尚、健身、音樂、自然、玩耍),而在《集合啦!動物森友會》中「a villager always has one hobby that doesn’t change」(村民永遠有一個不會改變的嗜好);村民會追蟲、追魚,卻「still will not catch either of them」(仍然一隻也抓不到),會運動、看書,會跟著附近的音樂播放器唱歌;自 2.0 更新起,他們會「invite the player to their house, ask the player for an invitation」(邀請玩家到家裡、請玩家邀請自己),或者「a random visit」(隨機來訪)。3132 八種個性決定他們說的話:男性村民有悠閒、運動、暴躁與自戀四種,女性村民有普通、元氣、成熟與大姊姊四種,其中「Smug and big sister were introduced in New Leaf, with the other six being present since Doubutsu no Mori.」(自戀與大姊姊是在 New Leaf 加入的,另外六種從《動物之森》起就存在。)33

整個系列的城鎮始終很小。32

遊戲 起始村民數 村民上限
Animal Crossing 6 15
Wild World 3 8
City Folk 6 10
New Leaf 5 10
《集合啦!動物森友會》 2(一位運動、一位大姊姊,從 83 位中選出) 10

Wild World 也改變了步調:在那一代「the villagers walk at a much slower pace than the player」(村民走得比玩家慢得多),City Folk 也沿用了這一點。32 這與《綠寶石》的規則恰恰相反,而依我的解讀,兩者奏效的理由相同:慢慢晃的村民看起來像居民,以您的步調走一步再等待的鎮民看起來也像居民。破壞這種效果的,是兩種步調都不是的人,而這正是第 6 節要談的 Kiradex 問題。

這個系列的預設表達方式是反應動作(Reactions),而它本身就是鎮民按每日配額送出的禮物。《集合啦!動物森友會》在 1.0.0 版有 44 種反應動作,到 2.0 版又多了 44 種,共 88 種,而且「The player can learn one Reaction per day,」(玩家每天可以學會一種反應動作),有些只能從特定個性的村民或在高好感度時學到。212

2. 其他人的痕跡:沒人上線時,是什麼填滿了小鎮

鎮民讓小鎮保持熱鬧,但每天都是同一批人。第二種答案,是每一款研究過的遊戲在擔心世界空蕩時都會伸手去拿的東西:用真人留下的痕跡填滿世界,而留下痕跡的玩家此刻不必在線。《綠寶石》是用一條連接線做到的。

紀錄混合:另一位玩家的世界,放在固定的欄位裡

兩位《綠寶石》玩家進行紀錄混合時,每一方的遊戲都會傳給對方一個固定結構 PlayerRecordEmerald,包含 11 種紀錄,共 0x1444 位元組,也就是 5,188 位元組。434 對於「有別人來過」的感覺而言,重要的是欄位數:20 座秘密基地、25 個電視欄位(5 個一般加 20 個額外)、16 則新聞、5 句流行語,以及遊戲保存的 4 位徒弟中的 2 位。4 依我的解讀,這正是這些欄位的意義:您的小鎮之所以改變,是因為別人玩過,朋友的基地、節目與流行語被帶進了您的遊戲。

最乾淨的例子是單一一個人。紫堇市的寶可夢中心裡有一位老人,他是五種老人之一(吟遊詩人、時髦青年、交易者、說書人與興奮老人,原文為 the Bard、the Hipster、the Trader、the Storyteller、the Giddy)。新遊戲會得到哪一位,由 (trainerId % 10) / 2 決定,原始碼本身的註解是「Determine man based on the last digit of the player’s trainer ID,」(依玩家訓練家 ID 的最後一位數決定是哪位老人),而在紀錄混合時,ReceiveOldManData 會用對方的老人覆蓋您的老人。435 您小鎮裡的居民,名副其實是另一位玩家的鎮民。

時髦青年依接觸來配給詞彙。他的腳本會設定 FLAG_UNLOCKED_TRENDY_SAYINGS,在他的 taughtWord 為 false 時教一個詞,然後把它設為 true;唯一會把它重新設為 false 的呼叫,是經由 ResetMauvilleOldManFlag 呼叫的 ResetHipsterFlag,而這只在一個地方發生:record_mixing.c 中 ReceiveOldManData 的結尾。35 所以每次紀錄混合帶來時髦青年,就會解鎖一句流行語,再加上一開始就有他的玩家可以解鎖一次,而依選擇規則,這是訓練家 ID 以 2 或 3 結尾的玩家。本系列的第一篇文章也是這樣陳述這條規則的。3635 詞彙是靠認識人而增加,不是靠等待。

流行語的算術

流行語是武鬥鎮當作當下流行說法的兩個簡易會話(Easy Chat)詞語,也是《綠寶石》唯一會計分的痕跡。它的原始碼註解說,一句「boring」(無聊)的流行語會「lose trendiness over time until it reaches 0, at which point it will stop being boring and gain trendiness until it reaches maxTrendiness (then it becomes boring again and the cycle repeats).」(隨時間失去流行度,直到降為 0;此時它不再無聊,開始累積流行度,直到 maxTrendiness,然後又變得無聊,如此循環。)37 一句流行語的峰值由最多三層巢狀的 Random() % 98 抽取,落在 30 到 127 之間。若把每次抽取視為在 98 個值上獨立且均勻分布,峰值的平均是 62.9,81.3% 的峰值在 80 以下(含);起始分數在 30 與峰值之間均勻分布,平均 46.5;分數每經過一天變動 5,與其他一切一樣在 UpdatePerDay 中結算。383 這些數字是對程式碼的近似,不是它的精確輸出:Random() 回傳 16 位元,所以餘數 0 到 71 在 65,536 次中出現 669 次,72 到 97 則出現 668 次,而且連續的抽取來自同一個產生器,並非獨立的骰子。依此對餘數加權(仍把各次抽取視為獨立),兩個數字在小數點後一位都不變。3837 若當成連續速率,峰值 30 的流行語從零升到峰值再降回零需要 12 天,峰值 64 需要 25.6 天,峰值 127 需要 50.8 天。遊戲是以整天移動分數,超過峰值或零的分數會以餘數反彈,所以每日分數並不會依那些帶小數的週期重複:從分數零、上升中開始逐日移植計算,確切的序列分別在 12、128 與 254 天後首次重複。3837 最多保存 5 句流行語,而在混合時,「their own trends are replaced with their mixing partner’s, unless the phrase is the same, in which case the version with a higher trendiness value is used.」(自己的流行語會被混合對象的取代,除非是同一句,這時採用流行度較高的版本。)3837

一張折線圖,呈現《綠寶石》流行語分數在 60 天中的變化,每一整天一個點,共三種峰值,各自從 0 開始、每天移動 5,在峰值與零處反彈。峰值 30 每 12 天升降一次,並精確重複。峰值 64 約每 25.6 天升降一次,這是連續近似;它的每日分數要到 128 天後才精確重複。峰值 127 約每 50.8 天升降一次;它的每日分數在 254 天後重複。

流行語是一條每天取樣一次的緩慢鋸齒線:81.3% 的峰值在 80 以下(含),也就是約 32 天以內完成一次升降。2638

依我的解讀,對一個必須讓小鎮不至於陳舊的痕跡來說,這是正確的形狀:它會上升、會下降,壽命在誕生時就已決定,而與另一位玩家的接觸可以用更新鮮的東西取代它。

簡易會話與聯機房間

《綠寶石》讓玩家說話的方式有兩種,它們正是本文安全論證的兩端。聯機房間(Union Room)是它的本地無線大廳,顯示 8 位小組領頭者,每個無線小組容納 5 位玩家(RFU_CHILD_MAX 的 4 再加 1),繪製 40 個 sprite,並定義 30 種活動代碼。它的聊天是打字的:四個鍵盤頁(UPPER、LOWER、EMOJI、REGISTER),每則訊息 15 個字元(MAX_MESSAGE_LENGTH),存檔中保存 10 句登錄短語(UNION_ROOM_KB_ROW_COUNT)。439 我的解讀是,打字聊天在那裡之所以可以接受,是因為房間裡的每個人都在您的無線通訊範圍內。

玩家之間說的其他一切,都經過簡易會話。它有 22 個群組,共 1,815 個詞條。一開始就開放的有:訓練家(27,其中 6 個停用)、狀態(109)、對戰(63)、問候(42)、人物(75)、聲音(63)、話語(60)、結尾(69)、感受(69)、狀況(69)、動作(78)、生活(45)、嗜好(54)、時間(45)、雜項(42)與形容詞(36),外加一份 202 個詞條的名稱清單,其中每個名稱要等玩家見過該物種才會開放;鎖住的則有事件(29)與兩份招式清單(154 與 200),要等通關才開放,流行語(33)要等時髦青年的旗標,還有一份 251 個詞條的全國名稱清單。3840 扣掉沒有玩家能選的 6 個停用訓練家詞條,從第一個小時起開放的詞就有 940 個,名稱不計。easy_chat.c 中的一個 switch 決定哪些群組解鎖,另一個決定每個詞條:名稱看它的物種是否見過,其他詞則看它的 enabled 欄位。3840 短語是固定的方格:個人資料是 2 乘 2,4 格;對戰開場白 2 乘 3,6 格;郵件 2 乘 5,10 格,最多放 9 個詞;流行語與好話 2 乘 1;問卷 2 乘 2,4 個詞。3840

一份 940 個詞、可以自由組合的詞彙表很有表現力,而依我的解讀,它也是一份有心的孩子可以用來拼出各種東西的詞彙表。這正是 Kiradex 規格書在第 7 節拒絕的取捨:完整的句子,絕不是詞語。

其他遊戲中陌生人的痕跡

其他每一款研究過、擔心世界空蕩的遊戲,都把手伸向了其他玩家,而且大多是伸向玩家留下的痕跡。來源的可信度各不相同,每一段都會註明自己的來源。

Dark Souls(FromSoftware,2011)。 Wikipedia 引用它自己的來源寫道:「The player can see ghostly images of other players, activate bloodstains that show how other players died, and leave messages using preset phrases.」(玩家可以看到其他玩家的幽靈影像,觸發顯示其他玩家如何死去的血跡,並用預設短語留下訊息。)41 《每日電訊報》(The Daily Telegraph)頒給它「Best Integration of Online Features」(最佳線上功能整合)獎。41 Bandai Namco Europe 的重製版頁面在主要特色中列出「The Way of the Multiplayer (up to 6 players with dedicated servers)」(多人遊玩之道,最多 6 名玩家,使用專用伺服器),對幽靈與訊息這一層則隻字未提;FromSoftware 自己的說明沒有取得。42 該頁面沒有說明那些幽靈影像是重播已離開的玩家,還是顯示當下連線中的玩家;它把幽靈影像與血跡、訊息一起列為遊戲整合(「integrates」)進「the single-player world」(單人世界)的「online features」(線上功能),而把「Direct multiplayer」(直接的多人遊玩),也就是召喚與入侵,放在下一句。41

Journey(thatgamecompany,2012)。 同樣是 Wikipedia:「In each level, the player may come across one other player temporarily connected to their game」(在每一關,玩家可能遇到另一位暫時連進自己遊戲的玩家);兩人「cannot communicate via speech or text and cannot see each other’s names until after the game’s credits」(無法以語音或文字溝通,在片尾字幕之前也看不到彼此的名字);「The only form of communication between the two is a musical chime.」(兩人之間唯一的溝通方式是一聲音樂鳴響。)開發者「felt having text or voice communication or showing usernames would allow players’ biases and preconceptions to come between them and the other player.」(認為若有文字或語音溝通、或顯示使用者名稱,玩家的偏見與先入之見就會橫亙在他們與另一位玩家之間。)5 這是以設計形式呈現的安全論證,而且是為成人而做的。

Death Stranding(Kojima Productions,2019)。 Sony 的頁面寫道:「Donate valuable resources to rebuild structures in your world and others’, and offer likes in support of player structures that appear in yours.」(捐出寶貴資源,重建您與他人世界中的建築,並對出現在您世界中的玩家建築按讚表示支持。)43 Wikipedia 補充,玩家「can leave supplies, structures, and messages that can be viewed and used by other players, although structures will eventually be destroyed by Timefall after some time」(可以留下補給、建築與訊息,讓其他玩家查看與使用,不過建築最終會在一段時間後被時間雨摧毀),而且「The player does not directly encounter other players in the world.」(玩家不會在世界中直接遇到其他玩家。)14 痕跡會衰退,所以世界不會被淤塞。

Splatoon(Nintendo,2015)。 依 Wikipedia 引用其來源的說法,玩家的 Miiverse 貼文會「appear in-game as graffiti on various buildings」(在遊戲中以塗鴉形式出現在各種建築上)。這些塗鴉來自遊戲 Miiverse 社群中的貼文,所以依我的解讀,這種痕跡仰賴的是 Nintendo 的 Miiverse 服務,而不是遊戲本身的線上遊玩。44

接下來三項較晚期的寶可夢功能,都只有一個來源:Serebii,一個歷史悠久的粉絲網站。pokemon.com 上《寶可夢 黑2/白2》與《精靈寶可夢 太陽/月亮》的頁面都沒有提到交流大道或節慶廣場,關於 Pokémon GO 道館也沒有存下任何發行商頁面,所以接下來三段都請當成 Serebii 的說法來讀,而不是發行商的說法。45

交流大道(Join Avenue,《寶可夢 黑2/白2》,2012)。 依 Serebii 的說法,這條大道「starts off as an empty pathway」(一開始是一條空蕩的小路),人們會來開店,而這「is done automatically whenever you connect with someone」(每當您與某人連線時就會自動發生),透過 PassBy、紅外線、聯機房間、Wi-Fi、GTS 等方式。共有八間店;「every 10th person shown to the shop, the shop will rank up」(每帶第 10 位客人到店裡,店就會升一級),最高到 Rank 10,「discounts of 1% for every rank up」(每升一級折扣 1%),直到「a 40% discount」(40% 的折扣);一般訪客價值「from 100 to 200 points」(100 到 200 點);打倒四天王之後「various fans will come and visit」(各式各樣的粉絲會來拜訪),而且「If you help a fan rather than another player, they will give 75% of the normal popularity points.」(如果您幫助的是粉絲而不是另一位玩家,他們會給予一般人氣點數的 75%。)13 依我的解讀,這是經典作品中對「替補規則」最乾淨的陳述:替身補上真人留下的空位,而且價值略低一些。

節慶廣場(Festival Plaza,《精靈寶可夢 太陽/月亮》,2016)。 依 Serebii 的說法,廣場「will propogate itself with players you will find online and locally」(原文如此,sic;會以您在線上與本地找到的玩家自行填滿),而且「This is not limited to people on your friend list」(這不限於您好友名單上的人);想要什麼的客人會說出來,您幫忙時會支付節慶幣,受過幫助的客人可以設為 VIP,讓他們「more likely to appear in your plaza in the future」(將來更有可能出現在您的廣場);廣場等級在 1 級時花費 6 枚幣,從 101 級起升到 300 枚。46 這是透過虛擬化身的在場:您互動的對象是另一位玩家的化身,而 Serebii 把化身背後的人描述為「all players that are also playing online」(同樣正在線上遊玩的所有玩家),這些人「when they see you」(看到您時)可以「challenge you to battles or request trades」(向您發起對戰或請求交換)。依這個說法,廣場上的人物代表的是本身也在線上的玩家;它與 Journey 是這份清單中,來源描述為即時在場而非痕跡的兩項功能。46

Pokémon GO 道館(2017 年的改版)。 依 Serebii 的說法,一座道館最多容納六位守護者;每輸一場對戰,守護者的幹勁就會下降,而且「Once their Motivation has dropped to 0, then they are removed from the Gym」(幹勁一旦降到 0,就會被移出道館);樹果可以恢復幹勁;守護者每 10 分鐘賺 1 枚寶可幣,「capped at 50 Coins earned a day.」(每天最多賺 50 枚。)47 這個地點會在另一位玩家離開時繼續展示他的夥伴,而除非社群持續餵食,它就會衰退。

依我對這八項的解讀,共同的模式是:少量固定的欄位(8 間店、6 位守護者、1 位夥伴、20 座基地),會衰退或輪替的痕跡(時間雨、幹勁、流行語的循環),價值低於真人的替身,以及除了節慶廣場的對戰與交換請求之外,陌生人之間除了一句預設短語或一聲鳴響,沒有任何即時管道。

3. 即時在場:頻率、緩衝與擦身而過的步行

第三種答案是大家最先想到的:其他玩家在線時,把他們呈現出來,在同一座廣場裡走動。Habbo 與 Club Penguin 這兩個小型 2D 社交世界,呈現了格狀在場的兩個極端。兩家公司都沒有公布它們的頻率。我手上有的是兩個開源的粉絲伺服器與一個粉絲用戶端,它們都是為了與原版用戶端相容而寫的,所以下面的數字證明的是那些用戶端預期的行為,而不是 Sulake 或 Disney 公布的數據。154849

Habbo:每個伺服器週期走一步

Arcturus Morningstar 是 Habbo 伺服器的開源重新實作,它讓每個房間以固定的 500 毫秒週期運行(scheduleAtFixedRate(this, 500, 500, TimeUnit.MILLISECONDS)),每個房間週期對每個步行中的單位呼叫一次 cycle;它在 IDLE_CYCLES = 240,也就是 120 秒時把化身標為閒置,並在 IDLE_CYCLES_KICK = 480,也就是 240 秒時把非房主移出。15 Nitro 是開源的 Habbo 用戶端,它以固定 500 毫秒的 DEFAULT_UPDATE_INTERVAL 移動房間物件:每收到伺服器的一次更新,就取得到新位置的變化量,依經過的時間除以 500 做線性插值,在終點處截止,然後把變化量歸零。49「一個週期恰好是一個圖塊」是我綜合兩者的解讀(500 毫秒的伺服器週期,畫成 500 毫秒的線性插值);我沒有讀伺服器的 RoomUnit 來證實。如果這個解讀成立,算繪在結構上就落後伺服器一個週期,所以只要伺服器跟得上,算繪就永遠不會缺資料。

Club Penguin:一個目的地,而不是一串資料流

Houdini 是 Solero 專案的開源 Club Penguin 伺服器,它展示了另一種設計。一次移動是一則 sp 訊息,攜帶一個目的地 x 與 y,原封不動地轉送給房間,由用戶端自己走過去。一句安全聊天是一則攜帶一個數字的 ss 訊息,其他種類的預設話語也以同樣方式傳遞:sj 是笑話,sma 是吉祥物的訊息,sl 是舞台台詞,sg 是導覽員的台詞,se 是表情符號,每一種都以 id 轉送。48 伺服器把表情與動作的頻率限制在每秒一次,畫格變更限制在每半秒一次,另有一個心跳任務每 61 秒檢查一次已連線的用戶端,關閉在這段時間內沒有送出心跳的連線。48 移動是點擊目的地,而不是逐格傳送;話語是一個在對方那端查表的數字。依我的解讀,這就是為什麼文字過濾器從來不必看到大部分說出口的內容。

這兩個世界的傳送頻率,都不是下面那些物理示範的每秒 10 到 60 次更新。16 對一個人們從一個圖塊走到下一個圖塊的世界來說,一步或一個目的地就是一次更新。

Gaffer on Games:在兩個已知點之間,落後著算繪

主導用戶端的方法,記載於 Glenn Fiedler 的〈Snapshot Interpolation〉(Gaffer on Games,2014年11月30日)。把收到的東西緩衝起來,在兩個延遲的更新之間算繪:「In effect, we’ve traded a small amount of added latency for smoothness.」(實際上,我們是用一點點額外的延遲換取了流暢。)16 他對延遲的經驗法則是「enough delay so that I can lose two packets in a row and still have something to interpolate towards」(延遲要夠長,讓我連續遺失兩個封包時仍有目標可以插值);在 2% 到 5% 的封包遺失率下,最好的做法「is 3X the packet send rate. At 10 packets per-second this is 300ms,」(是封包傳送間隔的 3 倍,每秒 10 個封包時就是 300 毫秒),再加上一兩個影格來吸收抖動,所以他的示範是以 350 毫秒運行。在「30 snapshots per-second」(每秒 30 個快照)時,同樣的保護需要 150 毫秒,而「60 packets per-second needs only 85ms.」(每秒 60 個封包只需要 85 毫秒。)16 外插,也就是往前猜,「doesn’t work very well for rigid bodies because their motion is non-linear and unpredictable.」(對剛體效果不太好,因為它們的運動是非線性且無法預測的。)16

Kiradex 的廣場透過 WebSocket 連到一台 FastAPI 伺服器,23 而 WebSocket 跑在 TCP 上,封包不會遺失,但重傳的封包會晚到。依我的解讀,這使得 WebSocket 世界的預算是抖動而不是遺失,也使得格狀行走者成為 Fiedler 方法中最簡單的情況:格狀行走者在圖塊中心之間直線移動並且說停就停,所以沒有東西需要外插,也沒有非線性的東西需要掩飾。

擦身而過的步行模型

為了替 Kiradex 決定緩衝的大小,我模擬了另一位收藏者從旁走過,所用的邏輯是 10月5日讀到的 app 自身的 rig。這支腳本 measure_remote_walk.py 模擬兩件事。一是今天的行為,採用 rig 自己的算術:一步的 progress 是 32 位元的 Float,每個影格增加 Float(deltaTime) 乘以每秒 4 格;每一則收到的移動都會從行走者最後的整數圖塊重新規劃路徑,並把進行中的一步歸零,即使正走到一半;超過 6 個圖塊的路徑會直接跳過去;行走者繪製在整數像素上。rig 以步行的每秒 4 格繪製其他每一位收藏者,因為只有玩家自己的步行會被標記為跑步。二是替代方案:在穩定的算繪時鐘上做快照插值,算繪時鐘是估計的伺服器時間減去 100 到 350 毫秒的延遲,在時間戳記夾住算繪時鐘的兩個已收到更新之間繪製。750 對今天的 rig,傳送端以今天量到的頻率,每走完一個圖塊送出一次移動:步行時每秒 4 格,每 250 毫秒一次;跑步時每秒 6.4 格,每 156 毫秒一次,這是跑步的名目速率(本系列的動態篇把 app 的跑步在 60 赫茲下建模為每秒 6.0 格,因為每個圖塊的超出量會被捨棄)。對緩衝,傳送端也以本系列的動作契約移動,也就是動態篇規格書在 60 赫茲下步行每格 16 tick、跑步每格 8 tick,即每秒 3.75 與 7.5 格,這也是第 7 節要建構的目標。網路加入 0、30 或 80 毫秒的均勻抖動,以每秒 60 個影格繪製 10 秒的步行。72122

算術很重要。在 32 位元下,15 個 1/60 秒的影格以每秒 4 格累加,恰好是 1.0,所以一步花 15 個影格,恰好是傳送端的 250 毫秒;在 64 位元算術中,同樣的加總是 0.9999999999999999,一步要花 16 個影格,以這種方式寫成的模型會在穩定步行中找到拉回,而 rig 自己的算術並不會產生這些拉回。7

這是一個模型,而它的假設正是應該把其數字當作規劃緩衝大小的參考、而非結果的理由。它假設一條筆直開闊的道路,所以路徑長度是曼哈頓距離;斜向一步在 rig 中要花 √2 倍的時間,這裡沒有建模;抖動是均勻的;每個影格的 deltaTime 恰好是 1/60 秒,而手機上的會變動;32 位元運算依程式碼的順序執行,沒有融合乘加;其中沒有任何東西是從兩台裝置上擷取的。所以,沒有抖動的穩定步行在手機上會不會被拉回,是這個模型無法定論的問題:影格時間只要稍微偏離 1/60,就可能讓一步在 1.0 之前差了一次捨入。重設本身,也就是只要移動在一步走到一半時抵達就會觸發的那個行為,是對原始碼的解讀,不是模型的結果。緩衝這一半還假設了兩件影響結果的事:算繪端對伺服器時鐘的估計是精確的,而且一次更新除了抖動之外,抵達不需要額外時間。真實的用戶端必須估計伺服器的時鐘,而這個估計的任何誤差,或任何它沒有計入的穩定單向延遲,都會直接從緩衝裡扣掉。7

兩個面板的分組長條圖,涵蓋六種情況:步行與跑步,各在 0、30 與 80 毫秒的抖動下。左邊是今天的 rig,傳送端步行每秒 4 格、跑步名目上每秒 6.4 格:步行時被畫成往後移動的影格分別為 0、7 與 16 個,跑步的三種情況都是 45 個。右邊是加上緩衝,以動作契約的每秒 3.75 與 7.5 格計算,600 個影格中沒有目標可繪製的影格數:步行在落後 100 毫秒時為 355、400 與 456,300 毫秒時為 0、0 與 27,325 毫秒時為 0、0 與 6,350 毫秒時為 0;跑步在 100 毫秒時為 125、188 與 304,300 毫秒以上為 0。每一次緩衝執行的往後影格都是 0。

在模型中,只要移動在一步走到一半時抵達,今天的 rig 就會把擦身而過的收藏者畫成往後移動,每一次跑步、每一次有抖動的步行都是如此;以動作契約的速度計算,350 毫秒的緩衝從不往後,也從不斷糧。26

今天的 rig 在沒有抖動的步行中能乾淨地畫出行走者:每一步都在下一則移動抵達前的那個影格結束,所以 40 個圖塊中拉回 0 次、跳躍 0 次、往後影格 0 個。抖動會破壞這一點。在 30 毫秒下,有 13 則移動在一步走到一半時抵達並重設它,其中 7 則抵達得夠晚,把行走者畫成往後移動,另有 1 條路徑長到需要跳躍;在 80 毫秒下,重設 25 次、往後影格 16 個、跳躍 2 次。跑步時,傳送端的速度超過 rig 繪製它的每秒 4 格,所以在每一種測試的抖動下,64 則移動中都有 45 則在一步走到一半時抵達,每一則都畫出一個往後的影格,並有 9 次跳躍。只要發生拉回,畫出來的行走者就會落後傳送端最多 5.9 到 6.7 個圖塊,直到一次跳躍把它拉上來;在穩定步行中,它從未落後滿一個圖塊(最多 0.94)。7 一次拉回會把行走者往後畫回它這一步已畫出的距離,在這些執行中,16 像素的圖塊上最多 14 像素。750

有了緩衝,行走者從不往後移動:60 次緩衝執行(四種速度乘以三種抖動乘以五種延遲)的往後影格全部是 0。唯一的問題是,算繪端多常沒有目標可以畫,這只在傳送端仍在傳送期間所繪製的 600 個影格中計算。以動作契約的步行每秒 3.75 格計算,落後 300 毫秒的算繪時鐘,在 0、30 與 80 毫秒的抖動下分別斷糧 0、0 與 27 個影格;325 毫秒斷糧 0、0 與 6 個;350 毫秒則完全不斷糧。落後 100 毫秒時,在這些速度的每一種情況下都斷糧 125 到 456 個影格。以每秒 7.5 格跑步時,300 毫秒以上在每一種測試的抖動下都斷糧 0 個影格。以今天的每秒 4 與 6.4 格計算,同一個模型在 300 毫秒時步行斷糧 0、0 與 7 個,在 325 或 350 毫秒時則完全沒有。7 依我的解讀,比估計伺服器時間落後 350 毫秒的算繪時鐘就是那個數字,而理由是一個上界,而不是一個計數。每個圖塊送出一次移動時,一個影格需要的那次更新,其時間戳記最多比算繪時鐘晚一步,而它抵達的時間最多比時間戳記晚最壞情況的抖動。以每秒 3.75 格計算,一步是 1/3.75 秒,即 266.7 毫秒,一步加上 80 毫秒的抖動是 346.7 毫秒,所以在時鐘精確的情況下,350 毫秒的延遲對任何最多 80 毫秒的抖動抽取都不會讓任何影格斷糧,還有 3.3 毫秒的餘裕。7 這 3 毫秒就是留給時鐘估計的全部容許量:估計值若比伺服器時鐘快超過約 3 毫秒,就失去這個保證,可能讓影格斷糧。上面的計數來自一次抖動抽取(種子 1),而 325 毫秒在那次抽取中以 4 個影格的餘裕達到了第 7.4 節設定的門檻,但並非每次抽取都如此:以種子 1 到 200 重新執行,在每秒 3.75 格步行、80 毫秒抖動下,325 毫秒在全部 200 次中都有斷糧的影格,其中 8 次超過 10 個,而 350 毫秒在任何一次都沒有斷糧。7 350 毫秒也正是 Fiedler 自己的示範所用的延遲。每個圖塊送出一次移動時,這比一個步行步伐的延遲稍長一點(每秒 3.75 格時一步是 267 毫秒),也就是把 Fiedler 的「3X the packet send rate」反過來,用在一個封包就是步伐、而且透過 TCP 時封包只會晚到而不會遺失的世界。16

4. 這門手藝,化為附數字的規則

下面每一條規則,都可以追溯到上文的一項量測或一段引述的來源。若某條規則是我綜合多個來源而得,來源欄會註明是哪些。

元素 規則 來源
誰在移動 始終在場的鎮民中,約六成的預設移動類型是靜止的,站在定點面向一個方向或四處張望,將近四成在定點周圍一到兩格的範圍框內遊走。來來去去的劇情演員大多站著,把整體比例推到七成與四分之一。 《綠寶石》沒有隱藏旗標的 100 人:59 人站立、39 人遊走、2 人原地踏步;全部 158 人:113 與 43;範圍 1 或 2 格1
步調 鎮民以玩家的步調走一步,然後等待比這一步更長的時間:一步 268 毫秒,接著隨機等待 0.54、1.07、1.61 或 2.14 秒。較慢的漫步也行得通(Wild World);兩者皆非的步調則行不通。 《綠寶石》共用的步行動作與延遲表1;Wild World32
移動占比 以每走一步計,遊走者處於移動中的時間最多 11% 到 33%;被擋住的一步只會轉身並等待。 《綠寶石》,以上面兩列計算;為上限127
人口 六到十個人就構成一座小鎮;《動物森友會》整座村子的上限是 8 到 15 人。 未白鎮 6 人、橙華市 7 人、紫堇市 10 人,其中分別有 2、3、6 人沒有隱藏旗標1;《動物森友會》的村子,最多 8 到 15 人32
變化 有些鎮民隨玩家的進度離開與到來,由旗標控制。 三座城鎮的 23 人中有 12 人帶有隱藏旗標1
作息 一天是三到七個定時點;由一把鍵決定當天適用哪一份,依具體程度的固定順序,先符合者勝出。 Abigail 的週三 3 個點,Pierre 的平日 7 個點;22 種鍵格式92
一天 在玩家回來時結算一天,以一次帶著經過天數的執行完成;絕不在無人遊玩時跑計時器。 UpdatePerDay3;《星露谷物語》的第一次每日更新9
記憶 以每位鎮民一個旗標記住當天的第一次交談;獎勵要小。 《集合啦!動物森友會》的 255 分中 +111;《星露谷物語》每顆愛心 250 點中的 +2010
新的詞 新的表達方式每天配給一個,或每次相遇一個。 每天一種反應動作12;時髦青年每來一次一句流行語35
痕跡 他人的痕跡放在少量固定的欄位裡,並會衰退或輪替。 20 座基地與 5 句流行語4;8 間店13;6 位守護者47;時間雨14;流行語一次升降 12 到約 50.8 天38
替身 替身補上真人留下的空位,價值較低。 交流大道的粉絲為 75%,依 Serebii 的說法13
陌生人 陌生人共享在場感,不共享言語:沒有名字,只有一聲鳴響或一句預設短語。 Journey5;Dark Souls41;兒童的 Game Center17
話語 預設台詞以 id 傳遞,而且能回應,不只是打招呼;完整的句子拼不出詞語清單能拼出的任何東西。 Club Penguin 的 ss id48;簡易會話開放的 940 個詞,置於 2 到 10 格的方格中38
更新 在格狀世界中每走一步更新一次;以比估計伺服器時間晚一點、略多於一個步行步伐的時鐘來算繪遠端行走者,絕不重設進行中的一步。 Habbo 的 500 毫秒週期與線性插值(模擬器與粉絲用戶端)1549;以每秒 3.75 與 7.5 格建立的模型,假設時鐘同步精確且無基礎延遲:落後 350 毫秒時,在最多 80 毫秒的抖動下,往後影格 0 個、斷糧影格 0 個7
閒置 玩家兩分鐘沒有輸入就標為閒置,並關閉不再回應心跳的連線。 Arcturus:240 個週期(120 秒)時標為閒置,480 個週期(240 秒)時移出閒置的非房主15;Houdini 會關閉在 61 秒時間窗內沒有送出心跳的用戶端48。規格書中「240 秒沒有 ping」結合了兩者,為我的調整
限制 Game Center 回報多人遊戲受限時(包括僅限好友),自訂多人功能就要拿掉,而依我的解讀,這包括真實玩家步行的重播:這樣的玩家只會看到鎮民。 Apple 對 isMultiplayerGamingRestricted 的文件18;把殘影視為多人功能是我的解讀(第 7.6 節)
溝通 Game Center 回報個人化溝通受限,或玩家未成年時,自訂溝通功能就要拿掉。 Apple 對 isPersonalizedCommunicationRestricted 的文件51
保留 兒童的資料只保留到其書面目的所需的時間為止,並在告知中載明刪除期限。 修訂後的 16 CFR 312.10;自 2026年4月22日起須遵守19

5. Apple 的做法:Game Center 的旗標、年齡範圍、審查準則與 COPPA

一座兒童可以走進來的小鎮,受到 Apple 與美國聯邦貿易委員會(FTC)規則的約束,而其中大多數附有日期。本節列出 Apple 與 FTC 自己的原文,Apple 有公布可用版本的也一併列出。我對某條規則對 Kiradex 意味著什麼的所有說法,都是解讀,並且標明為解讀,其中沒有任何一項是法律建議。

Game Center 的三個限制旗標

Game Center 的本機玩家帶有三個布林值,遊戲可以在開啟任何社交功能之前讀取。可用版本取自 2026年10月4日存下的 Apple 文件,各段說明逐字引用。185152

屬性 iOS Apple 的說法
GKLocalPlayer.isMultiplayerGamingRestricted 13.0 「If this property is true, the local player can’t join multiplayer games. If your game uses a custom multiplayer feature, you should disable it.」(此屬性為 true 時,本機玩家無法加入多人遊戲。若您的遊戲使用自訂的多人功能,應將其停用。)
GKLocalPlayer.isPersonalizedCommunicationRestricted 14.0 「If this property or the underage property is true, the local player can’t include personalized messages on invitations or enable voice communication in multiplayer games. If your game includes any custom communication features, you should disable them.」(此屬性或 underage 屬性為 true 時,本機玩家無法在邀請中附上個人化訊息,也無法在多人遊戲中啟用語音通訊。若您的遊戲包含任何自訂溝通功能,應將其停用。)
GKLocalPlayer.isUnderage 4.1 「If this property is true, Game Center disables some features for the local player.」(此屬性為 true 時,Game Center 會為本機玩家停用部分功能。)

第一個旗標的頁面補上了最關鍵的細節:這個值來自 Screen Time,家長可以在其中允許孩子與所有人、僅與好友,或不與任何人進行多人遊戲,而且「when you configure the setting to friends only, this property returns true for restricted.」(將設定設為僅限好友時,此屬性會回傳 true,表示受限。)18 所以,只被允許和好友一起玩的孩子,讀起來仍然是受限的,而一款有自己廣場的遊戲,被告知要把它關掉。第二個旗標的頁面有兩句話,適用範圍不同。第一句說的是 Game Center 自己封鎖的東西:邀請中的個人化訊息與語音;第二句則要求遊戲停用自己的「any custom communication features」(任何自訂溝通功能),並未對預設的功能設下例外。依我的解讀,一份由完整預設句子組成的選單就是一項自訂溝通功能,規格書也把它當成一項自訂溝通功能來處理。51

兒童的 Game Center,與 Screen Time

Apple 的 Game Center 隱私頁面訂下了 Kiradex 已對所有人採用的規則:「when communicating with other players in Game Center, children cannot send or receive user-inputted text. They are restricted to sending and receiving preset messages. In-game voice chat is disabled for children.」(在 Game Center 中與其他玩家溝通時,兒童無法傳送或接收使用者輸入的文字,只能傳送與接收預設訊息;兒童的遊戲內語音聊天會被停用。)17 頁面也說「Children’s accounts never make real names visible to friends,」(兒童帳號永遠不會讓好友看到真實姓名),而且家長可以用 Screen Time 封鎖「multiplayer functionality, the ability to add friends, and the ability to connect with friends playing the same game.」(多人功能、加好友的能力,以及與玩同一款遊戲的好友連線的能力。)17 Apple 的 Screen Time 支援頁面逐項列出 Game Center 的限制,其中包括「Connect with Friends」(與好友連線)與「Private Messaging」(私人訊息),每一項都是「Allowed or Blocked」(允許或封鎖)。53

Declared Age Range

2025年6月11日,Apple 宣布兒童帳號「is required for children under 13」(是 13 歲以下兒童的必要條件),並且「App developers will be able to request this information through the new Declared Age Range API」(App 開發者將能透過新的 Declared Age Range API 要求這項資訊),由家長選擇要「always, for each app request, or never」(一律、每次 app 要求時,或永不)分享孩子的年齡範圍,而且是「in a way that does not reveal the child’s birth date.」(以不透露孩子出生日期的方式。)54 同一則公告說,年齡分級將在該年底前「expanded to five categories」(擴充為五個類別),其中青少年有 13+、16+ 與 18+。54 Kiradex 的世界計畫把這個 API 列為啟用其少年規則的開關,並記錄了 10月2日的決定:依申報的範圍,13 歲以下完全沒有廣場;13 到 15 歲,廣場套用少年規則;16 歲以上或未申報,使用完整的廣場。23 撰寫本文時,我沒有取得這個 API 本身的參考頁面,所以這裡不列出它的最低 iOS 版本。

App Review 審查準則

有三條準則為社交廣場劃定了界線,引用自 Apple 標示為「Last Updated: June 8, 2026」(最後更新:2026年6月8日)的版本。55

  • 1.2,使用者產生的內容。「with user-generated content or social networking services must include」(含有使用者產生內容或社交網路服務的)app 必須包含過濾不當內容的方法、「A mechanism to report offensive content and timely responses to concerns」(檢舉冒犯性內容的機制,以及對疑慮的及時回應)、「The ability to block abusive users from the service」(從服務中封鎖惡意使用者的能力),以及「Published contact information so users can easily reach you.」(公開的聯絡資訊,讓使用者能輕鬆聯絡您。)最後變成「used primarily for pornographic content, Chatroulette-style experiences, random or anonymous chat」(主要用於色情內容、Chatroulette 式體驗、隨機或匿名聊天)以及該清單其餘項目的 app,「do not belong on the App Store and may be removed without notice.」(不屬於 App Store,可能會在未經通知的情況下被移除。)55
  • 1.3,兒童類別。 該類別中的 app「must not include links out of the app, purchasing opportunities, or other distractions to kids unless reserved for a designated area behind a parental gate,」(不得包含連到 app 外的連結、購買機會或其他會讓兒童分心的內容,除非放在家長閘門後的指定區域),而且「should not include third-party analytics or third-party advertising,」(不應包含第三方分析或第三方廣告),僅有少數例外。55
  • 5.1.4,兒童。 會「collect, transmit, or have the capability to share personal information (e.g. name, address, email, location, photos, videos, drawings, the ability to chat, other personal data, or persistent identifiers used in combination with any of the above) from a minor must include a privacy policy and must comply with all applicable children’s privacy statutes,」(向未成年人收集、傳輸或有能力分享個人資訊,例如姓名、地址、電子郵件、位置、照片、影片、繪圖、聊天能力、其他個人資料,或與上述任何一項結合使用的持久識別碼,就必須包含隱私權政策,並遵守所有適用的兒童隱私法規)的 app,而且「the parental gate requirement for the Kid’s Category is generally not the same as securing parental consent」(兒童類別的家長閘門要求,一般而言不等於取得家長同意)在那些法規下的意義。55

Kiradex 的計畫採取其設計文件從準則中歸納出的兩條路中的第二條:不進入兒童類別,分級 9+ 或 12+(分級尚未決定),適用 5.1.4。23 依我的解讀,5.1.4 清單中的「the ability to chat」(聊天能力),代表一座只有預設台詞的廣場,不論它避開了其他什麼,仍然需要隱私權政策。

修訂後的 COPPA

FTC 修訂後的《兒童線上隱私保護規則》(Children’s Online Privacy Protection Rule)於 2025年4月22日刊登於《聯邦公報》(Federal Register)(文件 2025-05904,90 FR 16918)。它「is effective June 23, 2025,」(自 2025年6月23日起生效),而且「Except with respect to § 312.11(d)(1), (d)(4), and (g), regulated entities have until April 22, 2026 to comply.」(除 § 312.11(d)(1)、(d)(4) 與 (g) 外,受規範的實體須於 2026年4月22日前遵守。)19 FTC 在 2025年1月16日以 5 比 0 表決通過後發布的新聞稿,總結了這次修改:向第三方揭露兒童資訊「related to targeted advertising or other purposes」(涉及目標式廣告或其他目的)時,須另外取得可驗證的家長同意;資料保留「for as long as reasonably necessary to fulfill a specific purpose,」(以達成特定目的所合理必要的期間為限),所以「operators cannot retain the information indefinitely.」(業者不得無限期保留這些資訊。)56

規則條文中有三段,與一座會記住任何東西的廣場有關。

  • 保留,§ 312.10。「Personal information collected online from a child may not be retained indefinitely. At a minimum, the operator must establish, implement, and maintain a written data retention policy that sets forth the purposes for which children’s personal information is collected, the business need for retaining such information, and a timeframe for deletion of such information.」(從兒童線上收集的個人資訊不得無限期保留。業者至少必須建立、實施並維護一份書面資料保留政策,載明收集兒童個人資訊的目的、保留這些資訊的業務需要,以及刪除這些資訊的期限。)這份政策要放進線上告知。19
  • 持久識別碼,§ 312.2。 個人資訊包括「A persistent identifier that can be used to recognize a user over time and across different websites or online services,」(可用於長時間、跨不同網站或線上服務辨識使用者的持久識別碼),例如「a customer number held in a cookie, an Internet Protocol (IP) address, a processor or device serial number, or unique device identifier.」(儲存在 cookie 中的客戶編號、網際網路協定(IP)位址、處理器或裝置序號,或唯一裝置識別碼。)19
  • 內部營運,§ 312.5(c)(7)。 在「Where an operator collects a persistent identifier and no other personal information and such identifier is used for the sole purpose of providing support for the internal operations of the website or online service,」(業者只收集持久識別碼、不收集其他個人資訊,且該識別碼僅用於支援網站或線上服務之內部營運)的情況下,不需要取得同意,但須告知。19

修訂後的 § 312.8 也要求為兒童資料制定一份書面的資訊安全計畫。19 我對這些規定對一座會重播訪客步行的廣場意味著什麼的解讀,寫在規格書中,並已標明;WORLD.md 第 3 節要求請律師審閱最終設計,而 10月2日的決定把這件事延到 app 有了起色之後,所以在那之前,這份解讀先保持原樣。23

代價

本節沒有任何一項是在裝置上量測的。驗證之後讀取 GKLocalPlayer 上的三個布林值,這裡沒有計時;一個 40 人的房間裡,為最多 39 位其他收藏者準備的 350 毫秒算繪緩衝,其記憶體與 CPU 用量也沒有計時。21 依我的解讀,兩者相較於繪製一個影格都無足輕重,但那只是解讀,而規格書的檢查針對的是行為,不是時間。

6. 案例研究:以 Kiradex 自己的程式碼量測它的廣場

Kiradex World 是 Kiradex 這款卡片收藏 app 裡的像素小鎮:一座 40 乘 30、以 16 像素圖塊繪製的廣場,由一台小型的 FastAPI 與 WebSocket 伺服器提供服務,伺服器把在場資訊放在記憶體中,什麼都不儲存,只有它的網頁伺服器預設會記錄的內容例外(第 7.6 節,設定 6)。212357 我在 10月5日從工作樹讀取了伺服器與 app 的世界程式碼,沒有修改任何東西,並以兩支腳本在模擬時鐘上驅動伺服器自己的類別,所以本節的數字是程式碼的行為,而不是我對程式碼的解讀。21620 程式庫是私有的,所以檔案以路徑標示。以下是 docs/WORLD.md 中的計畫、程式碼自己的 docstring,以及程式碼實際行為之間的差異;其中沒有任何一項是有人隱瞞的缺陷,而且計畫本身已經把第四項的一部分列為尚未建置。

現有的東西

伺服器保存 23 句預設台詞,14 句給玩家(索引 0 到 13),9 句給小鎮自己的收藏者(14 到 22);房間容量 40 人;每位玩家每秒最多 8 次移動、每 2.0 秒一句台詞;每 0.5 秒 tick 一次的小鎮時鐘(server/app/main.py 中的 TICK_SECONDS);向同一位收藏者展示卡片有 60 秒的冷卻時間;抵達點在 40 乘 30 廣場的圖塊 (19, 13)。2120 四位腳本驅動的收藏者住在伺服器上,讓房間裡的每個人看到同樣的人:Moss、Prism、Comet 與 Dawn,每位有 4 個路徑點,迴圈長度分別為 18、22、30 與 32 個圖塊,每位都有一種想被展示的卡片(復古、閃卡、任何卡、日文卡),以及 2 或 3 句台詞。server/app/npcs.py 設定 STEP_SECONDS = 0.7,讓每位收藏者在路徑點停留均勻分布的 3 到 8 秒,並每隔均勻分布的 20 到 40 秒說一句台詞,第一句在 8 到 20 秒之後。2120

依我的解讀,這是正確的直覺:鎮民在伺服器上而不是在每支手機裡,台詞在緩慢的時鐘上,每人各有一個想要的東西,讓收藏有了發揮的地方。量測之後,有三件事削弱了它,第四件則是計畫與程式碼之間的落差。

1. 步行走走停停

measure_npc_cadence.py 匯入 server/app/npcs.py 與 rooms.py,建立廣場與四位收藏者,並以實際的 0.5 秒 tick 推進它們一個模擬小時。6 同一段路程內兩步之間的 6,253 個間隔,每一個都恰好是 1.0 秒,而不是 0.7 秒:一步會在第一個時間等於或晚於其 next_move 的 tick 觸發,所以 0.7 秒被進位成兩個 tick。這是每秒 1.0 個圖塊,而常數所暗示的是 1.43。每段路程前的停頓平均 6.75 秒(4.5 到 9.0 秒,共 1,202 次),台詞平均間隔 30.7 秒,以同一位收藏者連續兩句台詞之間的 465 個間隔計算(這一小時共 469 句台詞)。6

用戶端以 250 毫秒繪製任何一步:PlazaRig.swift 在第 101 行把 tilesPerSecond 設為 4。50 所以一位每秒走一個圖塊的收藏者,在每段路程中有 25% 的時間被畫成移動中,另外 75% 的時間靜止不動,在每兩個圖塊之間都是如此。6 《綠寶石》的遊走者以玩家的步調走一步,然後等待;Kiradex 的收藏者在理應步行的時候,以玩家的步調一次衝一個圖塊,依我的解讀,這看起來既不像居民的漫步,也不像走路(第 1 節時間軸圖的最下面一列)。

2. 從旁走過的收藏者會被拉回

伺服器轉送另一位收藏者的移動時,PlazaRig.move(other:to:facing:)(PlazaRig.swift 第 526 到 542 行)會從行走者最後的整數圖塊規劃路徑,把行走者的步伐設為該路徑,並在每一則訊息中設定 progress = 0,即使一步只畫了一半;超過 6 個圖塊的路徑會直接跳過去。50 第 3 節的模型以 rig 自己的 32 位元算術,為此給出了數字。沒有抖動的穩定步行從不被拉回,因為每一步都恰好在下一則移動抵達前結束。在 30 或 80 毫秒的抖動下,40 個圖塊的步行會在一步走到一半時被重設 13 或 25 次,其中 7 或 16 次被畫成往後移動。跑步時,rig 以步行的每秒 4 格繪製行走者,所以在每一種測試的抖動下,64 則移動中都有 45 則在一步走到一半時抵達,並有 9 次跳躍;而只要發生拉回,畫出來的行走者就會落後傳送端最多 5.9 到 6.7 個圖塊。7 本系列較早的一篇文章 像素藝術的動態,從程式碼讀出了玩家自己在一步走到一半時再點一次所造成的同一種拉回;它的規格書所定的規則,絕不重設進行中的一步,正是這份規格書套用在其他收藏者身上的規則。22

3. 小鎮在空無一人時停止

server/app/npcs.py 的模組 docstring 這樣描述收藏者:「A town is never empty, and it has somewhere for a collection to matter.」(小鎮從不空蕩,而且有讓收藏發揮意義的地方。)20 server/app/rooms.py 中的 Room.tick 卻在自己的 docstring 裡說了相反的話:「A room with nobody in it stands still,」(沒有人的房間是靜止的),而且確實如此:房間裡沒有玩家時,它會立刻返回,所以收藏者就凍結在原地。206 腳本量測了第一位抵達者隨後看到的情況:以一次 tick 把剛建立的收藏者推進到 10,000 個模擬秒,會同時產生四句台詞,每位收藏者一句,人站在原來的位置;這段期間什麼都沒發生。6 而且因為伺服器什麼都不儲存,在安靜早晨第一個進來的收藏者,會看到同樣的四個人,卻看不到任何曾有其他人來過的跡象。搜尋 Kiradex/ 與 server/app/,找不到任何記錄造訪或每日交談的東西。24

4. 頻率,以及 Game Center 的旗標

有兩項差異存在於 docs/WORLD.md 與程式碼之間。計畫的第 5 節保留了促成這台伺服器的推論,其中的設計是用戶端以 8 到 10 赫茲傳送位置與朝向。23 app 實際上是每走完一個圖塊送出一次 move:今天步行時每秒 4 次,跑步時以名目速率每秒 6.4 次(動態篇對程式碼的模型在 60 赫茲下為 6.0),兩者都低於伺服器每秒 8 次的限制;本系列的動作契約,在 60 赫茲下步行每格 16 tick、跑步每格 8 tick,會讓它們成為 3.75 與 7.5,仍然在限制之下(第 7.4 節)。2122 依我的解讀,逐格傳送對格狀世界是較好的設計,規格書也保留它;需要更新的是計畫中的那一行。

計畫的第 3 節把身分建立在 Game Center 上,好讓「a child’s Game Center is controlled by the parent in Screen Time (multiplayer on/off, adding friends on/off, sharing the friend list with games on/off).」(孩子的 Game Center 由家長在 Screen Time 中控制:多人遊戲開/關、加好友開/關、與遊戲分享好友名單開/關。)23 Kiradex/ 或 server/app/ 中沒有任何地方讀取 GKLocalPlayer.isMultiplayerGamingRestricted、isUnderage 或 isPersonalizedCommunicationRestricted,也沒有呼叫 Declared Age Range API。24 計畫的第 5 節已經把「junior mode by Declared Age Range」(依 Declared Age Range 的少年模式)列為尚未建置的項目之一。23 Game Center 的旗標則是它沒有列出的部分:Screen Time 的多人遊戲設定控制的是 Game Center 自己的多人遊戲,而 Apple 的文件要求具有「a custom multiplayer feature」(自訂多人功能)的遊戲自行停用它,所以在 app 讀取這個旗標之前,家長的設定到不了廣場。18

用戶端在 socket 斷線後,以 min(30, 2^attempts) 秒的退避重新連線(PlazaClient.swift 第 165 行),而隱藏某位收藏者是在裝置上依 id 進行的。215023 這兩者在規格書中都維持原狀,規格書只為隱藏清單增加一個用途:每次請求廣場的殘影時一併送出,讓伺服器可以排除這位玩家已隱藏的收藏者的步行(第 7.2 節)。

7. 規格書:Kiradex 要建置什麼,以及必須通過的檢查

規格書保留廣場現有的東西(伺服器自己的收藏者、只有預設台詞、每個圖塊一次移動、在場資訊只放在記憶體中),並依經典作品給出的順序,加上三種答案:生活不會停止的鎮民、曾經在場者的痕跡,以及不會被拉回的在場呈現。每一項都附有一個可由腳本、測試或擷取畫面判定通過與否的檢查。圖塊座標採用廣場的座標,x 向右、y 向下。21

借用的界線,與本系列第一篇文章為專利與表現形式所劃的界線相同:不使用任何寶可夢生物、任何寶可夢地名、任何系列專有詞彙作為功能名稱,也沒有任何瞄準並投擲來捕捉、顯示捕捉機率、派夥伴去戰鬥,或登上任何東西騎乘的機制。36 下面的每一個元素,都是鎮民走路、站立、說完整的預設句子,以及其他收藏者步行的淡色重播。上文提到的遊戲系統,是以已出版作品的事實來陳述;Kiradex 的功能在這裡有 Kiradex 自己的名稱:鎮民、殘影、今日問候、台詞簿與安靜廣場。docs/WORLD.md 第 3 節已經把每一位不是好友的即時收藏者稱為路人;這個用語維持不變,而重播被稱為殘影,讓兩者永遠不會混淆。23 法律方面的說明是解讀,不是建議。

7.1 有作息的鎮民

  • 人口。 把廣場自己的收藏者從 4 位增加到 8 位,每位都有一個定點。21 比例依照《綠寶石》始終在場的人:100 人中 59 人站著,41 人沒有(39 人遊走、2 人原地踏步),換算成八人約為五人站著、三人移動。五人站在定點並會轉身:商店櫃檯、大廳門口、噴泉,以及兩個攤位。兩人在定點周圍 2 格的範圍框內遊走,就像《綠寶石》的遊走者。第三位移動者並非來自《綠寶石》的比例,因為那 158 位鎮民全都是站立、在範圍框內遊走或原地踏步,沒有一位走固定路線:它是今天四條路徑點迴圈中保留下來的一條,在定點之間繞行一圈,所以今天的四條迴圈變成一條。121 四位新收藏者以 app 自己的造型系統繪製,名稱取自 app 自己的詞彙清單,與今天的四位一樣。20
  • 等待。 定點上的收藏者轉身,以及遊走者走一步,都要在隨機挑選的 1、2、3 或 4 個伺服器 0.5 秒時鐘的 tick 之後:0.5、1.0、1.5 或 2.0 秒,也就是《綠寶石》的 0.54、1.07、1.61 與 2.14 秒取到最接近的 tick。以整數 tick 計的等待不會再被 tick 進位一次,而那正是第 6 節在步伐上量到的毛病。16
  • 房間的時鐘。 每個房間都以一個時鐘運行:伺服器的 UTC 時間,以房間自己的時區解讀,每個房間在伺服器設定中有一個 IANA 時區。伺服器在抵達時傳送這兩者,放在 welcome 與安靜廣場的回應中({"zone": "<IANA name>", "clock": <server ms>}),廣場開啟期間,app 從這兩者而不是從手機取得時段,讓房間裡的每個人在同樣的光線下看到同樣的鎮民。今天 Daylight.now 是從 Calendar.current 取得小時,也就是手機自己的日曆與時區,而伺服器只保有一個單調時鐘,沒有指定任何時區。50 每個房間使用哪個時區,以及收藏者被分到哪個房間,是這份規格書保留未定的設定;所有房間使用同一個時區是最簡單的起點。在廣場之外,收藏者仍以手機的時鐘為準。
  • 作息。 每位鎮民的一天是 3 到 7 個定時點,以一個字串寫在伺服器的資料中,由依序嘗試的鍵來選擇:日期、星期幾、時段,然後是預設值,每一項都以房間的時鐘解讀。9 時段是 app 自己的,來自 Kiradex/World/Daylight.swift 中的 Daylight.Phase:早晨 6 到 9 點,白天 9 到 17 點,傍晚 17 到 20 點,其餘時間是夜晚。50 在房間時鐘的夜晚,定點上的五人與兩位遊走者會穿過自己的門回到室內,就像《星露谷物語》中 Pierre 的週五,在酒館之後以「Returns home to sleep.」(回家睡覺。)作結;走保留迴圈的那位則縮短繞行的路線。30
  • 步調。 用兩種修改之一解決走走停停的步行。較佳的做法:伺服器一次送出一整段路程 {"t": "walk", "id": ..., "path": [...], "at": <server ms>},用戶端從 at 開始,以本系列動作契約下玩家的步行速度走完它,也就是在 60 赫茲下每格 16 tick,每秒 3.75 格(今天的 rig 是 4)。22 最低限度的做法:伺服器的步伐間隔是整數個 tick,STEP_SECONDS = 0.5,用戶端也以同樣的 0.5 秒繪製鎮民的一步,每秒 2 格,這是居民的漫步,而不是玩家的步行。632
  • 房間空無一人時的時間。 鎮民所在的圖塊是房間時鐘的純函數:由路線、時段與一個種子在被詢問時計算,而不是由 Room.tick 推進。種子是伺服器設定中的單一全域值,不是每個房間或時區各一個,所以時區相同的兩個房間,會在同樣的圖塊上顯示同樣的鎮民。依房間的時鐘在 07:40 第一個進來的收藏者,會看到 Moss 在 07:40 應該在的地方,而從設定為另一個時區的手機加入同一個房間的收藏者,看到的也一樣。63

檢查。 (a) 步調,依設計而定。對較佳的整段路程做法,一個用戶端測試餵入一則腳本化的 walk,路徑是 5 個圖塊的直線並附 at 時間戳記,在每一個 tick 記錄鎮民的位置(就像動態篇規格書的 -motionLog 記錄玩家的位置),並斷言:它在用戶端估計的房間時鐘上 at 所落在的那個 tick 出發,依序走過路徑上的圖塊,每個 tick 恰好移動 1 像素且從不往後,每格花 16 tick,並在 at 之後 80 tick 站在最後一個圖塊上;一個伺服器測試斷言每段路程只送出一次、完整、附有時間戳記。對最低限度的備案,一個伺服器測試以 0.5 秒 tick 驅動鎮民一個模擬小時,並斷言同一段路程內每一個步伐間隔都等於設定值;今天的 measure_npc_cadence.py 量到的是 1.0 秒,設定值卻是 0.7 秒。不論哪一種,都有一個伺服器測試斷言每一次等待都是整數個 tick。622 (b) 一個測試開啟兩個時區相同、相隔 600 秒的房間,兩者都使用同一個全域種子,並斷言在相同的伺服器時間,鎮民所在的圖塊完全相同;一個用戶端測試把手機時區設成與房間不同,並斷言廣場的時段依循房間的時鐘。(c) 一個測試把伺服器時鐘設為房間時區的 03:00 所對應的 UTC 時刻,並斷言定點上的五人與兩位遊走者不在廣場地圖上,而走保留迴圈的那位在。(d) 一個伺服器測試依類型清點這八人,得到定點 5 人、遊走 2 人、保留迴圈 1 人。

7.2 殘影:最近的八段步行

  • 機制。 廣場保留最近 8 次造訪。413 一次造訪是走過的路徑,以「圖塊與時間」的配對記錄,從抵達到走進第一扇門,或 60 秒,以較短者為準;行走者的造型不保留。房間裡的即時收藏者少於 8 位時,由殘影補上空缺:每個殘影以自己的步調重播它的步行,並在它走進的那扇門淡出。殘影沒有名牌,不舉起任何卡片,沒有點擊目標,也不說話。它是一段步行,而不是一個人。5
  • 外觀。 殘影的造型絕不從行走者的造型衍生。伺服器給每個殘影四種樸素造型之一,也就是清單中的預設部件配上四種樸素的染色,由工坊專為殘影製作,並畫成淡色(工坊的某一條染色色階,搭配固定的 alpha)。四種中挑哪一種,由以伺服器的密鑰對該步行自己的 salt 所做的金鑰雜湊決定,伺服器保管這個密鑰且從不傳送,所以同一位收藏者的兩段步行會得到彼此無關的造型。收藏者也可能穿著預設部件,所以剛好穿著這四種之一的行走者,可能湊巧與自己的殘影相同,而這種相同不帶任何意義:造型不透露是誰走的。58
  • 衰退。 殘影在 7 天後,或有 8 次更新的造訪到來時被刪除,以先發生者為準。14
  • 誰會看到殘影。 即時廣場中的收藏者,只要廣場中的收藏者少於 8 位。isMultiplayerGamingRestricted 為 true 的人則看不到:他們的安靜廣場只有鎮民,因為依我的解讀,殘影是廣場多人功能的一部分(第 7.6 節,設定 1)。18
  • 隱藏。 隱藏一位收藏者,也會隱藏他的殘影,而過濾由伺服器執行。每段儲存的步行都帶有一個隨機 salt 與一個標籤:以只有伺服器持有的密鑰,對行走者的 id 與該 salt 做的金鑰雜湊(HMAC),所以同一位收藏者的兩段步行帶有彼此無關的標籤。殘影的請求攜帶請求者已隱藏的 id;伺服器為每段儲存的步行重新計算每個被隱藏 id 的標籤,排除相符的步行,並在回應請求後丟棄這份清單,若是即時連線,則在 socket 關閉時丟棄。送到用戶端的是一個樸素造型、路徑與小時:沒有 id、沒有標籤、沒有 salt,也沒有任何取自行走者造型的東西。這移除了識別碼與外觀,而今天這兩者在每一次抵達時都一起傳送(server/app/rooms.py 中的 Player.public() 把 id、名稱與造型放在同一個物件中送出,server/app/main.py 再把它廣播給整個房間)。它並沒有移除步行本身。一條路徑、它的步調與它的小時,對那個小時在廣場上的人,或知道某位朋友總是去哪裡的人來說,可能指向某位收藏者;而在同一段步行期間待在房間裡的用戶端,已經在 moved 中收到每一個圖塊與行走者的 id。殘影是無名的,不是匿名的。2320
  • 儲存什麼,以及 COPPA 的解讀。 沒有名稱、沒有 Game Center id、沒有裝置 id、沒有行走者的造型、沒有地區,也沒有比小時更細的時間:只有圖塊、相對於步行開始的步伐時間、小時、salt(它也用來挑選樸素造型),以及用來套用隱藏的標籤。解讀,非建議: 這個標籤是從持久識別碼衍生而來的,所以我把它當成持久識別碼處理。它留在伺服器上,從不傳送,而且只用於套用隱藏這個唯一目的,我把這解讀為 § 312.5(c)(7) 所描述的內部營運用途,並須告知。傳送出去的步行沒有識別碼,造型由伺服器挑選,我把它解讀為在該規則對個人資訊的定義之外。這個解讀的弱點在於,步行是一種行為,那個小時看過廣場的人,可能把它和某位收藏者連起來;只到小時的時間、60 秒的上限與七天的期限縮小了這個可能,但沒有消除它。請求攜帶的被隱藏 id 只用於該次請求,既不記錄也不保留。隱私權告知中的書面保留政策寫明 7 天,正如 § 312.10 所要求。19 這讓伺服器從什麼都不儲存,變成把步行儲存一週,而這個解讀,應該交給計畫延到 app 有起色之後才請的那位律師。23
  • 誰會被記錄。 13 歲以下的人不會:依 10月2日的決定,他們完全沒有廣場。23 isMultiplayerGamingRestricted 為 true 的人不會(他們沒有可以走動的即時廣場,設定 1);isUnderage 為 true 的人不會,設定 2 已把它視為兒童訊號;依申報範圍為 13 到 15 歲的人也不會,這是計畫留待撰寫的少年規則中的第一條。185223
  • 不得類似。 血跡、重播的死亡、召喚符號、一整面玩家貼文的牆。殘影絕不是生物,也絕不戰鬥。4144

檢查。 (a) 一個伺服器測試記錄 10 次造訪,並斷言保留 8 次、最舊的被刪除,而且時鐘推進 7 天後一筆都不剩。(b) 一個結構測試斷言:儲存的步行恰好只有 path、hour、salt 與 tag 這幾個欄位,其中沒有名稱、id 或造型字串;用戶端收到的殘影恰好只有 figure、path 與 hour 這幾個欄位:沒有雜湊、標籤、salt 或 id 欄位。它也斷言殘影的造型字串,是以該步行的 salt 做金鑰雜湊所挑出的那一種,不論行走者穿什麼都一樣;對於穿著四種樸素造型以外造型的行走者,斷言殘影的造型不是行走者的造型。(c) 一個沒有即時同伴的 UI 測試顯示最多 8 個殘影,且沒有一個會回應點擊。(d) 分別以多人受限旗標、isUnderage,以及 13 到 15 歲範圍進行的測試,斷言伺服器沒有記錄那個用戶端的任何造訪。(e) 一個伺服器測試儲存兩位收藏者的步行,在請求殘影時隱藏其中一位,確認那位收藏者的步行被排除,然後確認被隱藏的 id 不在伺服器狀態中的任何地方。

7.3 今日問候

  • 機制。 每位鎮民都記得您今天有沒有跟他說過話。1110 當地日期的第一次交談會挑一句問候台詞(「Back again!」之類,為 Kiradex 而寫,加在 server/app/lines.py 的索引 22 之後);同一天之後的交談則挑一般台詞。21 您與同一位鎮民交談過的天數每累積三天,他就在一道隱藏的熟客階梯上前進一級,從您第一次交談起算,永不重設,而每前進一級,他就會說一句新台詞。不顯示任何量表。
  • 不衰退。 錯過一天或一個月,都不會失去任何東西。《星露谷物語》的好感度在您每一天沒交談時都會衰退;這裡永遠不會,因為 PEGI 把「daily quests, log-in streak or event-based rewards that expire after a while」(每日任務、連續登入,或會在一段時間後過期的活動獎勵)評為 PEGI 7,而在「players are punished by losing rewards or game status when they do not return in time」(玩家沒有及時回來就會因失去獎勵或遊戲地位而受罰)時評為 PEGI 12。105958
  • 存放位置。 在裝置上:收藏者個人資料中的一個字典,從鎮民 id 對應到最後一次交談的當地日期與交談過的天數,讓伺服器維持沒有每位玩家的儲存。

檢查。 (a) 一個單元測試在 2026-10-05 與一位鎮民交談兩次、在 2026-10-06 交談一次,得到兩句當天第一次的問候與一句一般台詞。(b) 一個單元測試把裝置日期往前、往後調整,確認階梯上的任何一級都從未被移除。(c) 搜尋 server/app,找不到為這項功能新增的任何每位玩家儲存。(d) 一個單元測試在相隔數週的三天各進行第一次交談,確認鎮民在階梯上前進了一級。

7.4 不會被拉回的在場

  • 傳送。 每走完一個圖塊送出一次 move,與今天相同。今天的基準是步行每秒 4 次,跑步每秒 6.0 到 6.4 次(分別是 60 赫茲下建模的跑步與其名目速率);在本系列的動作契約,也就是動態篇規格書在 60 赫茲下步行每格 16 tick、跑步每格 8 tick 之下,則是 3.75 與 7.5,而這份規格書要建構的正是那份契約。兩者都低於每秒 8 次的限制。2122 伺服器在它轉送的每一則 moved 上加上自己的毫秒時間戳記。
  • 算繪。 每個用戶端依收到的時間戳記,維持一個對伺服器時鐘的估計,並在比這個估計晚 350 毫秒的算繪時鐘上繪製其他每一位收藏者:在夾住算繪時鐘的兩個已收到時間戳記之間做線性插值,走路循環依走過的距離驅動。絕不重設進行中的一步。緩衝用盡時,停在最後一個圖塊上;格狀行走者說停就停,所以不做任何外插。只有在行走者落後超過 6 個圖塊或 2 秒時才跳躍。716 350 毫秒來自一個上界,並在一個假設時鐘估計精確、除抖動外沒有單向延遲的模型中檢查過(第 3 節):以動作契約的步行每秒 3.75 格計算,一步是 266.7 毫秒,一步加上 80 毫秒抖動是 346.7 毫秒,而至少這麼長的延遲,對任何抖動抽取都不會讓影格斷糧。這給時鐘估計的誤差留下約 3 毫秒;估計值比伺服器時鐘快得更多,就可能讓影格斷糧。325 毫秒在模型的第一次抖動抽取中通過了檢查 (a),在 80 毫秒時斷糧 6 個影格,但在 200 次抽取中有 8 次失敗;350 毫秒在任何一次都沒有斷糧。7 這是一個有待在裝置上擷取確認的起始值,不是結果。
  • 閒置。 收藏者 120 秒沒有輸入就標為閒置,以 rig 既有的 doze 閒置循環呈現,就像 Arcturus 在 240 個 500 毫秒週期時把化身標為閒置。1550 240 秒沒有 ping 就關閉 socket。這條規則是我對兩個來源的調整:ping 檢查來自 Houdini,它會關閉在 61 秒時間窗內沒有送出心跳的用戶端;240 秒則是 Arcturus 移出閒置非房主的時間長度,不過 Arcturus 計算的是沒有動作的週期,而不是遺漏的 ping。4815

檢查。 (a) 把新的邏輯移植到 measure_remote_walk.py,算繪時鐘取自用戶端對伺服器時鐘的估計,而不是傳送端自己的時鐘,並在傳送端以每秒 3.75 與 7.5 格移動時,要求六種情況的往後影格都是 0,在 0 與 30 毫秒抖動下斷糧影格為 0,在 80 毫秒下,傳送端持續傳送期間所繪製的 600 個影格中斷糧最多 10 個。時鐘精確的模型在 350 毫秒時,六種情況的斷糧影格都是 0,200 次抖動抽取中每一次都是如此。7 (b) 一個 UI 測試讓腳本化的同伴從廣場左邊直直走到右邊,擷取的影格中沒有任何一個同伴的 x 是減少的。(c) 一個伺服器測試斷言每一則轉送的 moved 都帶有時間戳記。(d) 一個伺服器測試把時鐘推進 241 秒且沒有 ping,並斷言 socket 已關閉。

7.5 台詞簿

  • 形狀。 六個分支,上線時每支 8 到 12 句,共 48 到 72 句,每一句都只要點兩下(先點分支,再點台詞):問候、回答(喜歡的年代、收藏了多久、哪個系列)、收藏、交換(安排交換,絕不定價)、加油,以及道別。今天是 14 句玩家台詞放在同一份清單裡。21 表情維持獨立的一列。Game Center 回報溝通受限,或未成年的玩家,完全不會得到台詞簿(第 7.6 節,設定 2)。
  • 成長。 最多再多 30 句可透過遊玩解鎖的台詞,每天最多一句,來自第 7.3 節的熟客階梯與等級。1235 清單由伺服器保管,可以在一小時內撤下一句台詞,因為它本來就會在抵達時送出清單。20
  • 與經典對照。 簡易會話開放 940 個詞,組合成 2 到 10 格的方格;《集合啦!動物森友會》有 88 種反應動作;《綠寶石》的聯機房間接受 15 個打字字元。38124 Kiradex 的台詞是完整的句子,從不由詞語組合而成,所以任何選擇的序列都拼不出名字、數字或地點。
  • 不得類似。 簡易會話的群組結構或它的詞語方格、流行語清單,或任何系列專有詞彙。40

提案中台詞簿的示意圖:一個根節點,有六個分支:問候、回答、收藏、交換、加油與道別,每支有八個已填入的台詞欄位與四個空欄位,每支 8 到 12 句。下方的註記:今天是 14 句玩家台詞放在同一份清單裡;《綠寶石》的簡易會話開放 940 個詞,置於 2 到 10 格的方格中;《綠寶石》的聯機房間是 15 個字元的打字訊息與 10 句登錄短語。

提案中的台詞簿:本文中唯一一張屬於提案而非量測結果的圖。26

檢查。 (a) 一個針對 lines.py 的測試斷言上線時有 6 個分支、每支 8 到 12 句,而且每一句距離根節點最多點 2 下。(b) 一個測試斷言沒有任何台詞包含數字或可自由填寫的空格。(c) 一個測試讀取 pret 的簡易會話詞語清單,確認沒有任何台詞等於某個詞條,也沒有任何兩個字以上的詞條出現在某句台詞中。(d) 一個單元測試在連續兩天各解鎖一句台詞,並拒絕同一天的第二次解鎖。

7.6 安全設定,每項都附有檢查

  1. Game Center 的限制會關掉其他收藏者,不論即時的或重播的。 GKLocalPlayer.local.isMultiplayerGamingRestricted 為 true 時(包括僅限好友),廣場以安靜廣場開啟:只有鎮民,沒有殘影,也沒有 WebSocket。鎮民與房間的時鐘透過一次請求取得,這次請求不攜帶帳號 id、裝置 id 或被隱藏的 id;伺服器只用請求的 IP 位址來回應它,廣場伺服器不記錄它;主機自己的邊緣請求日誌在這裡沒有驗證(設定 6)。18 解讀: 頁面寫道「If your game uses a custom multiplayer feature, you should disable it,」(若您的遊戲使用自訂的多人功能,應將其停用),而殘影是真實收藏者步行的重播,是第 2 節所整理的那種其他玩家留下的痕跡,而且是觀看者可能認得出來的痕跡(第 7.2 節)。Wikipedia 把 Dark Souls 中其他玩家的幽靈影像放在遊戲的「Multiplayer」標題下描述,但沒有說明它們是錄影,還是當時連線中的玩家。這個來源的立場並不一致:該標題下的文字說遊戲「integrates online features into the single-player world」(把線上功能整合進單人世界),並把「Direct multiplayer」(直接的多人遊玩)界定為合作召喚與入侵,與幽靈分開。規格書仍然選擇保守的一方:依我的解讀,殘影是廣場自訂多人功能的一部分,家長限制了多人遊戲的玩家一個都看不到。1841 能讓殘影對這位玩家重新開放的,是 App Review 或 Apple 以書面方式,把以樸素造型呈現、不附任何識別碼的重播步行,解讀為多人遊戲以外的東西;這份規格書不做這個假設。檢查: 一個把旗標設為 true 的 UI 測試斷言沒有開啟任何 WebSocket、沒有繪製任何殘影,而且廣場顯示它是安靜的;一個伺服器測試呼叫安靜廣場的路由,確認回應中沒有殘影,伺服器日誌中也沒有 IP 位址。
  2. 關閉自訂溝通。 isPersonalizedCommunicationRestricted 或 isUnderage 為 true 時,預設只有表情:台詞簿被隱藏,不送出任何 say,其他收藏者的台詞不為這位玩家繪製,而且永遠沒有語音、邀請訊息或打字欄位。5152 解讀: 頁面寫道「If your game includes any custom communication features, you should disable them,」(若您的遊戲包含任何自訂溝通功能,應將其停用),而一本預設句子的台詞簿就是一項自訂溝通功能。Game Center 在它自己的功能中允許兒童使用預設訊息,並不能讓遊戲自己的功能獲得豁免。5117 我保留表情,是因為揮手或點頭不帶任何兒童可用來聯絡他人的東西,但表情也算是某種溝通(Wikipedia 對 Journey 的描述,把它的鳴響稱為「The only form of communication between the two」,兩人之間唯一的溝通方式),而這是這份解讀中最弱的一步:如果 App Review 也把表情解讀為自訂溝通功能,表情就要拿掉,廣場對這位玩家就只剩在場感。5 為這位玩家保留台詞會是一個例外,需要 Apple 的書面同意;這份規格書不採取這個例外。檢查: 在 Kiradex/World 中搜尋 TextField、TextEditor 與 UITextField,結果為零;分別把每個旗標設為 true 的 UI 測試,顯示表情列、沒有台詞簿、不送出任何 say,也不繪製任何其他收藏者的台詞。
  3. 依 Declared Age Range 分齡,依 10月2日的決定。 13 歲以下:沒有廣場,只有系統產生的名稱。13 到 15 歲:廣場套用少年規則,其中第一條是他們的步行永遠不被記錄。16 歲以上或未申報:完整的廣場。2354 檢查: 一個單元測試,涵蓋三個年齡範圍與三個 Game Center 旗標的模式表。
  4. 檢舉與隱藏都觸及殘影。 隱藏一位收藏者,會隱藏他的即時造型,以及如第 7.2 節所述、在伺服器上依每次請求過濾的殘影;檢舉功能則保留在每位收藏者的面板上。55 殘影穿著樸素造型、沒有面板,所以玩家無法挑出被隱藏收藏者的殘影來隱藏它;這由伺服器的過濾完成。隱藏會觸及隱藏之前與之後儲存的步行;它觸及不到另一位玩家即時看到的東西,或從某段步行中認出的東西。檢查: 第 7.2 節的伺服器測試 (e),以及一個 UI 測試:隱藏一位腳本化的同伴、重播他已記錄的步行,並斷言它沒有被繪製。
  5. 沒有隨機配對,沒有匿名聊天。 陌生人只會共享廣場;沒有任何東西會把兩個陌生人配對進一個私人空間。55 檢查: 一個針對 server/app/main.py 處理器的測試,確認沒有任何訊息會在兩位不是 Game Center 好友的收藏者之間開啟管道。
  6. 以書面寫明保留政策。 隱私權告知加入書面資料保留政策:在場資訊只放在記憶體中;殘影 7 天,每段都附有一個行走者 id 的金鑰雜湊,它留在伺服器上,只用於套用隱藏,從不傳送;請求攜帶的被隱藏 id,不保留;連線的 IP 位址只用於回應它並路由回覆,我把這解讀為 § 312.5(c)(7) 下對內部營運的支援,而且廣場伺服器不記錄它;檢舉依原因與房間記錄。19 今天伺服器以預設值啟動 uvicorn 0.37.0(server/Procfile,--proxy-headers),而依我閱讀 uvicorn 本身的程式碼,這些預設值會把用戶端的轉送位址寫進日誌,對每一個 HTTP 請求(存取日誌預設開啟,除非關掉)以及每一個被接受的 WebSocket(uvicorn.error 上的一行 INFO)都是如此;規格書改以 --no-access-log --log-level warning 啟動它。57 檢查: 告知的 URL 可以從設定中開啟,第 7.2 節的伺服器測試 (a) 守住 7 天,而一個在正式環境啟動指令下開啟 WebSocket 並呼叫安靜廣場路由的測試,確認擷取到的日誌中沒有 IP 位址。主機的邊緣請求日誌不在這個測試的範圍內,這裡也沒有驗證:它們是否記錄用戶端的位址、保留多久,要在撰寫告知之前向主機確認。
  7. 世界中沒有第三方分析或廣告。55 檢查: 對 app target 的相依性稽核,沒有列出任何分析或廣告 SDK。

要拒絕的東西

  • 我們自己的遊玩計時器。 Screen Time 已經會限制 app 的使用時間,而 Club Penguin 的伺服器曾倒數孩子被允許的分鐘數,歸零時中斷連線;平台的限制才是家長已經熟悉的那一個。1748
  • 追蹤人數,以及任何「誰喜歡誰」的計數;計畫已經排除了這些。23
  • 附名字的訪客清單,一個列出誰來過的頁面。殘影刻意無名:它不帶 id,也不帶任何取自行走者造型的東西。它不是匿名的:那個小時在廣場上的人,或熟悉某位朋友習慣的人,仍可能認出一段步行,這就是為什麼殘影只帶小時、沒有更細的時間,也是為什麼沒有可以讀取殘影的清單。
  • 可以點擊的殘影,或可以與之交談、跟隨的殘影。
  • 今日問候的量表,或任何因為沒有回來而失去的東西。59

不在這份規格書中

  • 讓人布置的房間。 家具、帽子,以及造訪其他收藏者的房間,屬於本系列即將推出的一篇文章。
  • 新台詞的措辭。 規格書只定下台詞簿的形狀;句子為 Kiradex 而寫,另行在地化。
  • 團體遊玩。 交換、展示與分享,以及計畫列為尚未建置的交換畫面,都不在這份規格書的範圍內。23
  • 來自所量測遊戲的任何東西。 不使用來自寶可夢、《星露谷物語》、《動物森友會》、Dark Souls、Journey、Death Stranding、Splatoon、Habbo 或 Club Penguin 的任何生物、地名、標誌、圖塊或聲音。
  • 裝置上的時間。 檢查針對的是行為、擷取畫面與一個模型;這裡沒有任何東西保證影格時間。

重點整理

如果您負責繪製美術

  • 畫站著的人要比畫走路的人多。在《綠寶石》地圖上始終存在的鎮民中,六成站在定點,大多面向一個方向,有些四處張望(把劇情演員算進去則是七成);為每個站著的人物畫出朝向與張望的動作,把走路循環留給將近四成的遊走者。1
  • 讓另一位玩家步行的殘影看起來像痕跡,而不是一個人:淡色、無名、手上什麼都沒拿、在一扇門前淡出。它的畫法中不該有任何東西引人點擊。5

如果您負責建置引擎

  • 在抵達時結算小鎮的一天,以一次帶著經過天數的執行完成,並從房間裡每個人共用的一個時鐘,而不是從每支手機的時鐘,計算鎮民所在的圖塊;房間沒人就返回的伺服器迴圈會凍結小鎮,Kiradex 量到的正是如此。36
  • 讓伺服器的步伐間隔是整數個 tick,並以被給定的間隔繪製每一步;在 0.5 秒 tick 上的 0.7 秒步伐,每 1.0 秒才落地一次,又只用 0.25 秒畫完,結果就是一段有 75% 時間站著不動的步行。6
  • 每個圖塊送出一次更新,在伺服器上加上時間戳記,並以比估計伺服器時間晚 350 毫秒的時鐘繪製其他行走者,絕不重設進行中的一步;在模型中,以本系列的每秒 3.75 與 7.5 格、時鐘估計精確的情況下,最多 80 毫秒的抖動時,往後影格 0 個、斷糧影格 0 個。7
  • 開啟 socket 之前,先讀取 isMultiplayerGamingRestricted、isPersonalizedCommunicationRestricted 與 isUnderage;僅限好友也會讀成受限,而依我的解讀,受限的玩家也不應該看到其他玩家的重播。185152

如果您負責設計遊戲循環

  • 把他人的痕跡放在少數幾個會衰退的固定欄位裡:八段步行保留七天,就足以表達有人來過。在伺服器上套用隱藏,傳送痕跡時不帶 id、使用樸素造型而不是行走者自己的造型,並清楚說明,看過那段步行的人仍可能認出它。414
  • 用一個旗標記住今日問候,絕不因為錯過一天而收回任何東西;PEGI 會對會過期的連續登入評級。59
  • 給陌生人句子,而不是詞語:一本由完整句子組成的台詞簿,無法被拿來拼出任何東西,而 Game Center 本來就把兒童限制在預設訊息。Game Center 表示溝通受限時,連台詞簿也一併拿掉。381751

常見問題

沒有其他玩家在線時,要怎麼讓遊戲小鎮感覺熱鬧?

給它有自己作息的鎮民,以及曾經在場者留下的痕跡。在《寶可夢 綠寶石》的 16 座城鎮中,沒有隱藏旗標的 100 位鎮民裡,59 位站在定點,39 位在一到兩格的範圍框內遊走,2 位原地踏步(若把旗標可移除、大多站著的劇情演員算進去,在全部 158 人中分別占 71.5%、27.2% 與 1.3%),他們以玩家的步調走一步,每步之間等待 0.54 到 2.14 秒;《星露谷物語》的村民過著由幾個定時點組成的日子,由 22 種鍵格式選出;而《綠寶石》的紀錄混合會把另一位玩家的基地、流行語與居民複製到您的小鎮裡。124

在俯視視角的像素遊戲中,NPC 應該多久移動一次?

在《綠寶石》中,遊走者走一步花 16 個影格(268 毫秒,與玩家步行是同一個動作),然後等待 32、64、96 或 128 個影格,所以以每走一步計,處於移動中的時間最多 11% 到 33%,若它挑的方向被擋住則更少;大多數鎮民的預設移動類型從來不走動,不過劇情腳本仍然可以讓他們走。《動物森友會》的 Wild World 則反其道而行,讓村民走得比玩家慢。看起來不對的,是兩者皆非的步調:Kiradex 的收藏者以玩家的速度一次衝一個圖塊,每兩個圖塊之間停 0.75 秒。1326

《寶可夢 綠寶石》的紀錄混合是什麼?

一種透過連接線進行的交換,每一方的遊戲都把一個固定結構傳給對方,包含 11 種紀錄、共 5,188 位元組,其中有 20 座秘密基地、25 個電視欄位、16 則新聞與 5 句流行語。對方紫堇市的老人會取代您的老人,而時髦青年的到來,是唯一能讓他再教一句流行語的事。435

2D 多人遊戲需要多少插值延遲?

Glenn Fiedler 的規則是:足以撐過兩個遺失的封包,約三個傳送間隔,再加上一兩個影格來吸收抖動。16 透過跑在 TCP 上的 WebSocket,封包不會遺失,晚到的封包會被重傳,所以依我的解讀,預算是抖動而不是遺失的封包,而略多於一個間隔的緩衝就可能足夠。對每個圖塊送出一次更新的格狀世界,一個以本系列步行每秒 3.75 格、跑步 7.5 格模擬 Kiradex 步行的模型發現,比估計伺服器時間晚 350 毫秒的算繪時鐘,在最多 80 毫秒的抖動下,往後影格 0 個、斷糧影格 0 個;在一次抖動抽取中,300 毫秒在 80 毫秒抖動下步行時斷糧 600 個影格中的 27 個,325 毫秒斷糧 6 個,而 100 毫秒在每一種情況下都斷糧 125 到 456 個;在 200 次抽取中,350 毫秒在任何一次都沒有斷糧。350 毫秒建立在一個上界上:每秒 3.75 格時的 266.7 毫秒步伐,加上 80 毫秒的抖動,是 346.7 毫秒。這是一個假設道路筆直、抖動均勻、時鐘估計精確且沒有基礎延遲的模型,不是裝置上的擷取。7

兒童可以在 iOS 遊戲中使用聊天嗎?

Game Center 說兒童「cannot send or receive user-inputted text」(無法傳送或接收使用者輸入的文字),並且「restricted to sending and receiving preset messages」(只能傳送與接收預設訊息),語音聊天也被停用。Apple 的文件要求具有「any custom communication features」(任何自訂溝通功能)的遊戲,在 isPersonalizedCommunicationRestricted 或 isUnderage 為 true 時停用這些功能,而準則 1.2 要求使用者產生內容必須具備過濾、檢舉、封鎖與公開的聯絡資訊。那句話沒有對預設訊息設下例外,所以依我的解讀,只要任一旗標為 true,遊戲自己的預設台詞就要拿掉,即使 Game Center 在它自己的功能中允許兒童使用預設訊息;Kiradex 的規格書對這樣的玩家只保留表情,而這個解讀是我的,不是 Apple 的原文。175155

COPPA 允許儲存其他玩家的移動,當成幽靈重播嗎?

我不是律師,這是解讀,不是建議。修訂後的規則把「A persistent identifier that can be used to recognize a user over time」(可用於長時間辨識使用者的持久識別碼)算作個人資訊,允許業者在告知的前提下,單獨使用持久識別碼「for the sole purpose of providing support for the internal operations」(僅用於支援內部營運),並且自 2026年4月22日起,要求一份載明刪除期限的書面保留政策。Kiradex 的設計儲存一段步行,不帶名稱、帳號 id、裝置 id,也不帶行走者的造型,另附一個行走者 id 的金鑰雜湊,它留在伺服器上,只用於套用其他玩家的隱藏,從不傳送;殘影以伺服器挑選的四種樸素造型之一繪製,兩者都保留七天,並寫進那份政策。我把這個雜湊視為用於內部營運的持久識別碼,把傳送出去的步行視為在定義之外,但步行是一種行為,那個小時看過廣場的人可能把它和某位收藏者連起來,所以殘影是無名的,而不是匿名的,最終設計應該由律師審閱。19

本站相關文章:iPhone 上的像素藝術世界 是本系列的第一篇指南,涵蓋圖塊系統與本文小鎮所在的廣場,它的法律常見問題劃出了規格書所遵循的專利界線;像素藝術人物 建立了收藏者與工坊,殘影所穿的樸素造型就出自這個工坊,文中也引用了讓今日問候不會衰退的 PEGI 規則;像素藝術建築 建立了鎮民夜裡回家時穿過的門;像素藝術的動態 量測了掌機的步行、為 app 自己的步行建立模型,並定下規格書套用在其他收藏者身上的「絕不重設一步」規則;iPhone Duo 開發者指南 則介紹了廣場所要繪製的那台裝置。

資料來源


  1. 作者的量測,measure_npcs.py,存放於作者為本文準備的證據資料夾中,2026年10月5日修訂並重新執行,輸出存為 measure_npcs.out。輸入:pret 的 pokeemerald 反編譯,commit 731ad5b 的淺層複製(master,2026年10月1日):include/constants/event_object_movement.h(81 種移動類型)、16 個城鎮的 data/maps/*/map.json 檔案(物件事件、graphics id、移動類型、範圍、隱藏旗標),以及 src/event_object_movement.c(步進表、以 59.7275 赫茲計時的三張延遲表、哪種移動類型設定哪種延遲,以及 MovementType_WanderAround_Step4)。修訂版依 graphics_id 把物件事件分成人物、物件(6 個 ITEM_BALL、2 個 TRUCK、1 個 MR_BRINEYS_BOAT)與生物(5 個),若出現尚未分類的 graphics id 就停止執行;VAR_0 與 VAR_3 事件的 sprite 來自一個變數,因為它們每一個都帶有勁敵的隱藏旗標,所以算作人物。較早的版本把全部 172 個物件事件都算成鎮民;它與它的輸出以 .r1 字尾保留在旁邊,而修訂版的輸出原封不動地重複較早的各行。第一次修訂及其輸出以 .r2 字尾保留;目前的腳本是第二次修訂,依物件事件中的隱藏旗標把 158 人分開:有旗標的 58 人(54 人站立、4 人遊走;其中 41 人使用反派組織、勁敵或有名字角色的 sprite,依腳本中一份固定的 graphics id 清單判定)與沒有旗標的 100 人(59 人站立、39 人遊走、2 人原地踏步),並重新計算三座城鎮中沒有隱藏旗標的人數(未白鎮 2、橙華市 3、紫堇市 6);它的輸出原封不動地重複所有較早的各行。沒有隱藏旗標的人在每次載入時都在地圖上;把那 58 人稱為劇情演員,是我依他們的 sprite 與旗標名稱所做的解讀,不是資料中的欄位。移動占比(11% 到 33%)是對步進列與延遲列的算術,16 個影格除以 16 加 32 到 128,以每走一步計:這是上限,因為選定方向被擋住的遊走者會不移動、重新轉身並等待。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  2. 作者的量測,measure_daily.py,存放於同一資料夾,2026年10月5日重新執行,輸出與 measure_daily.out 相同。輸入:pret pokeemerald 731ad5b 的 include/constants/flags.h 與 src/clock.c;存下的 Stardew Valley Wiki 頁面「Villagers」(oldid 191346,編輯於2026年2月26日)、「Pierre」與「Modding:Schedule data」(22 種鍵格式依頁面的表格計數);以及 Nookipedia 的「Reaction」頁面(《集合啦!動物森友會》依版本的數量)。 ↩↩↩↩↩↩↩↩↩↩↩

  3. pret,pokeemerald/src/clock.c(DoTimeBasedEvents,以及 UpdatePerDay,它帶著 daysSince,也就是自上次執行以來的天數,執行一次),commit 731ad5b;DoTimeBasedEvents 也會在 src/overworld.c 中於地圖載入時被呼叫,並由 src/field_tasks.c 中的一個週期性場地任務呼叫,2026年10月4日存取,https://github.com/pret/pokeemerald/blob/master/src/clock.c。 ↩↩↩↩↩↩↩

  4. 作者的量測,measure_record_mixing.py,存放於同一資料夾,2026年10月5日重新執行,輸出與 measure_record_mixing.out 相同。輸入:pret pokeemerald 731ad5b 的 src/record_mixing.c(struct PlayerRecordEmerald、它的 11 個成員與 0x1444 位元組的大小)、include/constants/global.h 及相關標頭檔(SECRET_BASES_COUNT、TV_SHOWS_COUNT、POKE_NEWS_COUNT、SAVED_TRENDS_COUNT、APPRENTICE_COUNT)、src/mauville_old_man.c(五種老人與選擇規則),以及 src/union_room_chat.c、include/union_room.h 與 include/constants/global.h(聯機房間的領頭者、小組大小、sprite、活動代碼、鍵盤頁、MAX_MESSAGE_LENGTH 與 UNION_ROOM_KB_ROW_COUNT)。 ↩↩↩↩↩↩↩↩↩↩↩↩

  5. Wikipedia,「Journey (2012 video game)」,Gameplay 與 Development 兩節,每項陳述各自引用來源,2026年10月4日存下,https://en.wikipedia.org/wiki/Journey_(2012_video_game)。thatgamecompany 自己的 Journey 頁面也存下了,但只有導覽內容。 ↩↩↩↩↩↩↩

  6. 作者的量測,measure_npc_cadence.py,存放於同一資料夾,2026年10月5日修訂並重新執行,輸出存為 measure_npc_cadence.out;修訂版只增加了對每一則 said 訊息的計數(這一小時共 469 則),放在 30.7 秒平均值所依據的 465 個「同一位收藏者連續兩句台詞之間的間隔」旁邊,其他各行不變(前一版以 .pre-astra 字尾保留)。它從 2026年10月5日讀取的工作樹匯入 Kiradex 伺服器自己的 app.npcs(STEP_SECONDS、make_all)與 app.rooms(Plaza)(檔案與雜湊值見註 20),從 server/app/main.py 讀取 TICK_SECONDS,並以種子 1 在理想的 0.5 秒時鐘上推進四位收藏者 3,600 個模擬秒,記錄每一則 moved 與 said 訊息。連續兩步之間 1.5 秒以下的間隔算作同一段路程內;更長的間隔算作停頓。空房間測試檢查 Room.tick 的提前返回,並對剛建立的收藏者呼叫一次 advance(10000.0)。25% 與 75% 的數字,是以用戶端 250 毫秒的一步(PlazaRig.tilesPerSecond = 4)除以量到的 1.0 秒間隔而得。這次執行取代了 measure_kiradex.py 僅從常數推導出的 0.7 秒與 36%。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  7. 作者的模型,measure_remote_walk.py,存放於同一資料夾,2026年10月5日修訂並重新執行,輸出存為 measure_remote_walk.out;前一版以 64 位元浮點數進行 rig 的算術,它與它的輸出以 .pre-f32 字尾保留。修訂版把 rig 中以 Float 進行的每一項運算都以 32 位元(NumPy float32)、依程式碼的順序執行:progress 是 Float(PlazaRig.swift 第 29 行),update 接收 Float(event.deltaTime)(PlazaStage.swift 第 149 行),advance() 加上 dt * tilesPerSecond * (running ? runPace : 1) / length(第 660 行),而 running 只在玩家身上設定(第 614 行);位置是 (Float(x) + 0.5) * 16(TileMap.swift 第 235 行),繪製於 from + (to - from) * progress,取整到整數像素,遇到一半時遠離零進位(第 670 與 726 行)。它會印出檢查結果:15 個 1/60 秒的影格在 32 位元下加總恰好是 1.0,在 64 位元下則是 0.9999999999999999。它依 10月5日讀到的內容重新實作 PlazaRig.move(other:to:facing:)(從行走者最後的整數圖塊規劃路徑;每則訊息都設定 progress = 0;路徑為空或超過 6 步時跳躍),並把「仍有剩餘步伐且 progress 超過 0.05 時的重設」計為一次拉回,把「繪製的像素位置小於前一個影格」計為一個往後影格,並依繪製的像素位置計算落後量。另外,它在穩定的算繪時鐘上模擬快照插值,rt = now - delay,延遲為 100、250、300、325 與 350 毫秒,其中 now 是傳送端的時鐘,所以模擬的規則是「估計的伺服器時間減去延遲」,而不是「最新的更新減去延遲」;這一半是提案中的算繪器,不是 rig,並以 64 位元浮點數執行。假設:每個影格的 deltaTime 恰好是 1/60 秒;沒有融合乘加;傳送端與算繪端的時鐘完全同步;沒有基礎的單向延遲,所以一次更新在其時間戳記加上抖動時抵達;一條筆直開闊的道路(路徑長度是曼哈頓距離;斜向步伐在 rig 中要花 √2 倍的時間,這裡沒有建模);傳送端每走完一個圖塊送出一次移動,兩個半部都採用今天的步行每秒 4 格與跑步的名目速率 6.4,緩衝那一半另外也採用本系列的動作契約,每秒 3.75 與 7.5 格;0 到 J 毫秒的均勻抖動,J 為 0、30 與 80;每秒 60 個影格;種子 1。rig 每種情況執行 12 個模擬秒(傳送 10 秒、穩定 2 秒),每次緩衝執行 660 個影格(11 秒),斷糧影格只在傳送端仍在傳送期間繪製的 600 個影格中計算。第二支腳本 measure_remote_walk_seeds.py,存放於同一資料夾,2026年10月5日執行,輸出存為 measure_remote_walk_seeds.out,它從 measure_remote_walk.py 的原始碼載入同一個插值函式,斷言種子 1 重現其 27、6 與 0 個斷糧影格,並以種子 1 到 200,在每秒 3.75 與 7.5 格下重新執行緩衝:在 80 毫秒抖動、步行時,325 毫秒在全部 200 個種子上都有斷糧影格,其中 8 個超過 10 個,350 毫秒則一個都沒有,而在 325 與 350 毫秒下的其他每一種情況都沒有斷糧。它印出 350 毫秒背後的上界:每秒 3.75 格時一步是 266.67 毫秒,加上 80 毫秒抖動是 346.67 毫秒,留下 3.33 毫秒。它也把算繪時鐘偏移,當作比伺服器時鐘快的時鐘估計;在這個影格與步伐都對齊 1/60 秒網格的模型中,快 10 毫秒在 200 個種子上仍然都沒有斷糧,快 25 毫秒則表現得像 325 毫秒,所以正文中的 3 毫秒是上界所提供的保證,而不是這個模型開始斷糧的那一點。這是一個模型,不是從兩台裝置上擷取的結果。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  8. pret,pokeemerald/data/maps/,16 個城鎮的 map.json 檔案,其中包括 LittlerootTown、PetalburgCity 與 MauvilleCity,以及 data/maps/PetalburgCity/scripts.inc(道館前的男孩,LOCALID_GYM_BOY,在 map.json 中為 MOVEMENT_TYPE_LOOK_AROUND 且沒有隱藏旗標,由 PetalburgCity_Movement_BoyWalkToGym 的移動帶到道館),commit 731ad5b,2026年10月4日存取,https://github.com/pret/pokeemerald/tree/master/data/maps。 ↩↩↩↩

  9. Stardew Valley Wiki,「Modding:Schedule data」,2026年10月4日存下,https://stardewvalleywiki.com/Modding:Schedule_data。作息格式、遊戲 1.5.1 版時 Abigail 的原始資料、鍵格式及其順序,以及「Limitations」說明。 ↩↩↩↩↩↩↩↩

  10. Stardew Valley Wiki,「Friendship」,2026年10月4日存下,https://stardewvalleywiki.com/Friendship。每顆愛心的點數、好感度增減的方式、衰退表、禮物、生日倍數與愛心事件。 ↩↩↩↩↩↩

  11. Nookipedia,「Friendship」,《集合啦!動物森友會》一節,2026年10月4日存下,https://nookipedia.com/wiki/Friendship。 ↩↩↩↩

  12. Nookipedia,「Reaction」,《集合啦!動物森友會》一節,2026年10月4日存下,https://nookipedia.com/wiki/Reaction。依版本的數量來自 measure_daily.py(註 2)。 ↩↩↩↩↩

  13. Serebii.net,「Join Avenue」,Pokémon Black 2 and White 2 一節,2026年10月4日存下,https://www.serebii.net/black2white2/joinavenue.shtml。一個粉絲網站,也是這項功能唯一使用的來源。 ↩↩↩↩↩

  14. Wikipedia,「Death Stranding」,Gameplay 一節,引用其自身的來源,2026年10月4日存下,https://en.wikipedia.org/wiki/Death_Stranding。 ↩↩↩↩↩

  15. Krews,Arcturus Morningstar,一個由粉絲撰寫的 Habbo 伺服器開源模擬器,src/main/java/com/eu/habbo/habbohotel/rooms/Room.java(IDLE_CYCLES = 240、IDLE_CYCLES_KICK = 480、scheduleAtFixedRate(this, 500, 500, TimeUnit.MILLISECONDS)),2026年10月5日存下,https://git.krews.org/krews/Morningstar/-/blob/master/src/main/java/com/eu/habbo/habbohotel/rooms/Room.java。這些是模擬器的常數,不是 Sulake 公布的數據;RoomUnit 沒有讀。 ↩↩↩↩↩↩↩

  16. Glenn Fiedler,「Snapshot Interpolation」,Gaffer on Games,2014年11月30日,2026年10月4日存下,https://gafferongames.com/post/snapshot_interpolation/。 ↩↩↩↩↩↩↩↩

  17. Apple,「Game Center & Privacy」,「Children in Game Center」一節與好友一節,2026年10月4日存下,https://www.apple.com/legal/privacy/data/en/game-center/。 ↩↩↩↩↩↩↩↩

  18. Apple Developer Documentation,GKLocalPlayer.isMultiplayerGamingRestricted(iOS 13.0+),2026年10月4日存下的文件 JSON,https://developer.apple.com/documentation/gamekit/gklocalplayer/ismultiplayergamingrestricted。引文是 Discussion 的文字,其中對 true 的行內參照以單字呈現,由草稿資料夾中的 sources/extract_gk.py 攤平。 ↩↩↩↩↩↩↩↩↩↩↩↩

  19. Federal Trade Commission,「Children’s Online Privacy Protection Rule」,最終規則,《聯邦公報》(Federal Register),2025年4月22日,文件 2025-05904,90 FR 16918,2026年10月4日存下,https://www.federalregister.gov/documents/2025/04/22/2025-05904/childrens-online-privacy-protection-rule。DATES 一節,以及修訂後的 16 CFR 312.2(個人資訊的定義)、312.5(c)(7)、312.8 與 312.10。本文中關於這條規則對 Kiradex 意味著什麼的每一項陳述,都是作者的解讀,不是法律建議。 ↩↩↩↩↩↩↩↩↩↩↩

  20. 作者對 Kiradex 程式庫(私有)的閱讀,2026年10月5日的工作樹,唯讀,未執行任何 git 指令;檔案以其 SHA-256 的前八個十六進位數字識別:server/app/main.py(87c6cc9f;TICK_SECONDS = 0.5、SHOW_EVERY_SECONDS、各訊息處理器,其中包括第 223 行攜帶行走者 id 與圖塊的 moved 轉送)、server/app/npcs.py(d84f12a5;模組 docstring、STEP_SECONDS = 0.7、四個定義及其路線、想要的東西與台詞)、server/app/rooms.py(dcd66866;第 115 行的 Player.public(),它在同一個物件中回傳 id、名稱與造型,並在 main.py 第 203 行於抵達時廣播給房間;CAPACITY、MOVES_PER_SECOND、SAY_EVERY_SECONDS、Plaza,以及 Room.tick 與它的 docstring 和提前返回)、server/app/lines.py(391f5391;23 句台詞,以及說明伺服器會在抵達時送出清單的 docstring)。 ↩↩↩↩↩↩↩↩↩↩↩

  21. 作者的量測,measure_kiradex.py,存放於同一資料夾,2026年10月5日重新執行,輸出與 measure_kiradex.out 相同。它從註 20 的工作樹匯入 app.lines、app.rooms 與 app.npcs,並讀取 PlazaRig.swift 與 PlazaClient.swift(註 50)以取得用戶端的常數:預設台詞數量、房間容量、頻率限制、tick、冷卻時間、廣場大小與抵達點、四位收藏者的路線與迴圈長度、步行與跑步速度、6 步的跳躍門檻,以及重新連線的退避。它推導出的 0.7 秒與 36% 已被註 6 取代。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  22. Blake Crosley,「Pixel-Art Motion: Walks, Cameras and Doors on iPhone」,blakecrosley.com,2026年10月5日,https://blakecrosley.com/blog/pixel-art-motion-on-iphone。它的註 6 以精確的 60 赫茲模擬 app 的步行:跑步每格 10 個影格,每秒 6.00 格,而每秒 4 格乘以跑步的 1.6 所暗示的是 6.4,在 120 赫茲下則是 6.32;它關於現有程式碼的一節,從 walk(to:) 讀出一步走到一半時再點一次所造成的拉回,而它的規格書定下了進行中的一步絕不重設的規則;它的規格書第 1 項把步行定為每格 16 tick、跑步每格 8 tick(60 赫茲下),即每秒 3.75 與 7.5 格,也就是本系列的動作契約,而它的檢查以 -motionLog 啟動參數在每個 tick 記錄玩家的位置。 ↩↩↩↩↩↩↩

  23. Kiradex,docs/WORLD.md(「Kiradex World: research and plan」),2026年10月5日的工作樹(SHA-256 c7714bc1),唯讀:第 2 節(兒童類別與 5.1.4 的路徑,分級 9+ 或 12+)、第 3 節(Game Center 身分、Screen Time 控制、沒有追蹤人數、預設台詞、律師的審閱)、第 5 節(10月2日建置的伺服器:FastAPI 與 WebSocket、只有在場資訊、什麼都不儲存、每秒八次移動、在裝置上隱藏、「Not yet」(尚未)清單中包括依 Declared Age Range 的少年模式;以及促成它的推論,其中用戶端以 8 到 10 赫茲傳送位置與朝向),以及「Decisions taken (Blake, October 2, 2026)」中的第 6 項(分級與年齡範圍)與第 8 項(在有起色之前不請律師)。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩

  24. 作者於2026年10月5日在 Kiradex 工作樹的 Kiradex/ 與 server/app/ 中搜尋 isMultiplayerGamingRestricted、isUnderage、isPersonalizedCommunicationRestricted、Declared Age Range API,以及任何造訪或每日交談的儲存:沒有符合的結果。 ↩↩↩

  25. pret,pokeemerald/include/constants/event_object_movement.h,commit 731ad5b,2026年10月4日存取,https://github.com/pret/pokeemerald/blob/master/include/constants/event_object_movement.h。 ↩

  26. 圖表由 figures/make_figures.py 繪製,該腳本保存在本文草稿旁邊,2026年10月5日,資料取自 measure_npcs.py、measure_npc_cadence.py、measure_chat_trend.py、measure_record_mixing.py、measure_kiradex.py 與 measure_remote_walk.py 存下的輸出;每一個數值都從這些檔案解析出來,並在繪製前與一個預期值比對斷言:預期值來自研究檔案,但城鎮人口圖除外,它現在只繪製修訂後 measure_npcs.py 中的人物,並加上一列沒有隱藏旗標的 100 人;台詞簿的 940 個開放詞則來自修訂後的 measure_chat_trend.py。在第二個引擎的審查之後,流行語圖改為繪製修訂後 measure_chat_trend.py 中遊戲自己的整數日分數(每經過一天一個點,而不是連續的三角波),擦身而過的步行圖則繪製修訂後的 measure_remote_walk.py:以今天的 rig 自己的 32 位元算術計算,以及以動作契約的每秒 3.75 與 7.5 格、延遲 100、300、325 與 350 毫秒計算的緩衝。台詞簿圖是規格書提案的示意圖,其中的對照數字以同樣方式解析。沒有使用任何遊戲美術、sprite 或地圖算繪。 ↩↩↩↩↩

  27. pret,pokeemerald/src/event_object_movement.c(IsCoordOutsideObjectEventMovementRange、sMovementDelaysMedium、sMovementDelaysShort、帶有 // Unused 註解的 sMovementDelaysLong、兩種旋轉類型固定 48 個影格的等待、MovementType_WanderAround_Step4、GetWalkNormalMovementAction、各 sStep 表),commit 731ad5b,2026年10月4日存取,https://github.com/pret/pokeemerald/blob/master/src/event_object_movement.c。 ↩↩↩↩↩

  28. pret,pokeemerald/include/constants/flags.h(DAILY_FLAGS_START 與 12 個每日旗標),commit 731ad5b,2026年10月4日存取,https://github.com/pret/pokeemerald/blob/master/include/constants/flags.h。 ↩

  29. Stardew Valley Wiki,「Villagers」,oldid 191346,編輯於2026年2月26日,2026年10月4日存下,https://stardewvalleywiki.com/Villagers。34 位可送禮的村民,是依頁面上的清單計數(6 位單身漢、6 位單身女子、22 位非結婚候選人);wiki 的「Friendship」頁面計算的是另一組 28 位起始鎮民。 ↩

  30. Stardew Valley Wiki,「Pierre」,2026年10月4日存下,https://stardewvalleywiki.com/Pierre。平日作息、六種作息變化,以及在任何愛心等級與三顆愛心時寄出的信件。 ↩↩↩

  31. Nookipedia,「Hobby」,2026年10月4日存下,https://nookipedia.com/wiki/Hobby。 ↩

  32. Nookipedia,「Villager」,各代遊戲的章節,2026年10月4日存下,https://nookipedia.com/wiki/Villager。 ↩↩↩↩↩↩↩

  33. Nookipedia,「Villager」,Personalities 一節,2026年10月4日存下,https://nookipedia.com/wiki/Villager#Personalities。 ↩

  34. pret,pokeemerald/src/record_mixing.c(struct PlayerRecordEmerald、ReceiveOldManData、ReceiveDewfordTrendData),commit 731ad5b,2026年10月4日存取,https://github.com/pret/pokeemerald/blob/master/src/record_mixing.c。 ↩

  35. pret,pokeemerald/src/mauville_old_man.c(選擇用的 switch ((trainerId % 10) / 2) 及其註解、ResetHipsterFlag、ResetMauvilleOldManFlag、SetHipsterTaughtWord)、pokeemerald/src/record_mixing.c(對 ResetMauvilleOldManFlag 的唯一一次呼叫,位於 ReceiveOldManData 的結尾)與 pokeemerald/data/scripts/mauville_man.inc(setflag FLAG_UNLOCKED_TRENDY_SAYINGS、special HasHipsterTaughtWord),以及 pokeemerald/data/maps/MauvilleCity_PokemonCenter_1F/map.json(腳本為 MauvilleCity_PokemonCenter_1F_EventScript_MauvilleOldMan 的物件事件),commit 731ad5b,作者於2026年10月4日與5日閱讀,https://github.com/pret/pokeemerald/blob/master/src/mauville_old_man.c。時髦青年出現在訓練家 ID 以 2 或 3 結尾的遊戲中,是作者對該 switch 所做的算術。 ↩↩↩↩↩↩

  36. Blake Crosley,「Pixel-Art Worlds on iPhone: What the 16-Bit Masters Knew」,blakecrosley.com,2026年10月3日(法律常見問題更新於2026年10月4日),https://blakecrosley.com/blog/pixel-art-world-on-iphone。它把流行語規則陳述為每次紀錄混合帶來時髦青年時解鎖一句,而它的法律常見問題,以其自身的來源列出了對 Palworld 開發商主張的三項專利(瞄準並投擲捕捉道具、捕捉機率指示器、登上可騎乘的角色)。 ↩↩

  37. pret,pokeemerald/src/dewford_trend.c(關於 trendiness 與 maxTrendiness 以及保存流行語的開頭註解),commit 731ad5b,2026年10月4日存取,https://github.com/pret/pokeemerald/blob/master/src/dewford_trend.c。 ↩↩↩↩

  38. 作者的量測,measure_chat_trend.py,存放於同一資料夾,2026年10月5日修訂並重新執行,輸出存為 measure_chat_trend.out。修訂版從開放的詞中扣除標記為 .enabled = FALSE 的詞條(946 個詞條,940 個啟用),印出 easy_chat_groups.h 中訓練家群組的 numEnabledWords 那一行(「Excludes Red, Green, Flame, Gold, Leaf, and Silver」,排除 Red、Green、Flame、Gold、Leaf 與 Silver),並印出 IsEasyChatIndexAndGroupUnlocked 中的逐詞條規則,依該規則,202 個詞條名稱清單中的名稱只有在其物種見過後才開放;較早的版本及其輸出以 .r1 字尾保留。輸入:pret pokeemerald 731ad5b 的 src/data/easy_chat/(群組大小、停用的詞條、numEnabledWords)、src/easy_chat.c(群組解鎖的 switch、逐詞條規則與短語方格)、include/constants/global.h(MAIL_WORDS_COUNT、NUM_QUESTIONNAIRE_WORDS、SAVED_TRENDS_COUNT),以及 src/dewford_trend.c(SeedTrendRng 與每日步進)。第二次修訂是在第二個引擎的審查之後(前一版及其輸出以 .pre-astra 字尾保留),它把峰值分布標示為「獨立均勻」近似:它由三層巢狀的 Random() % 98 抽取計算而得,每次抽取都視為在 0 到 97 上獨立均勻。它另外加上以 16 位元取模權重計算的同一分布(src/random.c 中的 Random() 回傳高 16 位元,所以餘數 0 到 71 在 65,536 次中出現 669 次,72 到 97 出現 668 次),得到平均 62.89、80 以下(含)81.34%,對照 62.90 與 81.33%,仍然假設各次抽取獨立;它也以每次呼叫經過一天的方式移植 UpdateDewfordTrendPerDay,從分數 0、上升中開始,對峰值 30、64 與 127,分別在 12、128 與 254 天後首次回到該狀態(從每一個可達的起始狀態出發,循環長度都相同)。12、25.6 與 50.8 天的升降長度,是峰值的兩倍除以 5,為連續近似。 ↩↩↩↩↩↩↩↩↩↩↩↩

  39. pret,pokeemerald/src/union_room_chat.c、pokeemerald/include/union_room.h 與 pokeemerald/include/constants/global.h,commit 731ad5b,2026年10月4日存取,https://github.com/pret/pokeemerald/blob/master/src/union_room_chat.c。關於打字聊天為何在那裡可以接受的解讀,是作者的。 ↩

  40. pret,pokeemerald/src/easy_chat.c 與 pokeemerald/src/data/easy_chat/,commit 731ad5b,2026年10月4日存取,https://github.com/pret/pokeemerald/blob/master/src/easy_chat.c 與 https://github.com/pret/pokeemerald/tree/master/src/data/easy_chat。 ↩↩↩↩

  41. Wikipedia,「Dark Souls (video game)」,Multiplayer(引用其來源 [5])與 Awards 兩節,2026年10月4日存下,https://en.wikipedia.org/wiki/Dark_Souls_(video_game)。 ↩↩↩↩↩↩

  42. Bandai Namco Entertainment Europe,「Dark Souls Remastered」,Key features,2026年10月5日存下,https://en.bandainamcoent.eu/dark-souls/dark-souls-remastered。Bandai Namco America 的頁面存下時只是一個腳本外殼,文字僅 127 位元組;FromSoftware 自己的說明沒有取得。 ↩

  43. PlayStation,「Death Stranding」,「Unique social strand gameplay」一節,2026年10月4日存下,https://www.playstation.com/en-us/games/death-stranding/。 ↩

  44. Wikipedia,「Splatoon (video game)」,Gameplay 與 Multiplayer 兩節,引用其自身的來源,2026年10月4日存下,https://en.wikipedia.org/wiki/Splatoon_(video_game)。Nintendo 自己的 Splatoon 頁面沒有取得。 ↩↩

  45. The Pokémon Company International,pokemon.com 上「Pokémon Black Version 2 and Pokémon White Version 2」與「Pokémon Sun and Pokémon Moon」的遊戲頁面,2026年10月4日存下,https://www.pokemon.com/us/pokemon-video-games/pokemon-black-version-2-and-pokemon-white-version-2 與 https://www.pokemon.com/us/pokemon-video-games/pokemon-sun-and-pokemon-moon;兩者都沒有提到 Join Avenue 或 Festival Plaza。沒有嘗試 Bulbapedia。 ↩

  46. Serebii.net,「Festival Plaza」,Pokémon Sun and Moon 一節,2026年10月4日存下,https://www.serebii.net/sunmoon/festivalplaza.shtml。一個粉絲網站,也是這項功能唯一使用的來源。 ↩↩

  47. Serebii.net,「Gyms」,Pokémon GO 一節,2026年10月4日存下,https://www.serebii.net/pokemongo/gyms.shtml。一個粉絲網站,也是這項功能唯一使用的來源。 ↩↩

  48. Solero,Houdini,一個由粉絲撰寫的 Club Penguin 伺服器開源模擬器,houdini/handlers/play/player.py(sp、ss、sj、sma、sl、sg 與 se 處理器及其冷卻時間;server_heartbeat,它取得當下時間,睡眠 61 秒,然後關閉最後一次心跳早於該時間的每一個用戶端(if penguin.heartbeat < timer: await penguin.close(),讀自存下的 HTML;存下的文字擷取把那一行弄亂了);以及 server_egg_timer,它每分鐘倒數一次玩家被允許的分鐘數,在 7 與 5 時警告,歸零時中斷連線),2026年10月4日存下,https://github.com/solero/houdini/blob/master/houdini/handlers/play/player.py。這證明的是原版用戶端預期的行為,不是 Disney 公布的行為。 ↩↩↩↩↩↩↩

  49. billsonnn,Nitro renderer,一個由粉絲撰寫的 Habbo 開源用戶端,src/nitro/room/object/logic/MovingObjectLogic.ts(DEFAULT_UPDATE_INTERVAL = 500 與 update 中的線性插值),2026年10月4日存下,https://github.com/billsonnn/nitro-renderer/blob/main/src/nitro/room/object/logic/MovingObjectLogic.ts。 ↩↩↩

  50. 作者對 Kiradex 程式庫(私有)的閱讀,2026年10月5日的工作樹,唯讀:Kiradex/World/PlazaRig.swift(SHA-256 819cd8d6;第 29 行作為 Float 的 progress、第 101 行的 tilesPerSecond、第 526 到 542 行的 move(other:to:facing:)、第 614 行只在玩家身上設定的 running、第 652 到 672 行的 advance()、第 722 到 727 行的 place() 及其像素取整、含有 doze 的 idleEmotes 清單)、Kiradex/World/TileMap.swift(db4272c2;第 60 行為 16 的 tileSize 與第 235 行的 centre)、Kiradex/World/PlazaClient.swift(5c491933;第 165 行的重新連線延遲)、Kiradex/World/PlazaStage.swift(8b4850af;套用到廣場的日光時段,以及第 149 行的 rig.update(Float(event.deltaTime))),以及 Kiradex/World/Daylight.swift(536350dc;Daylight.Phase 及其小時範圍,以及從 Calendar.current,也就是裝置自己的日曆與時區取得小時的 Daylight.now)。同一天也搜尋了伺服器的時鐘:server/app/main.py 與 server/app/rooms.py 使用 time.monotonic(),server/app 中沒有任何地方指定時區。 ↩↩↩↩↩↩↩↩↩

  51. Apple Developer Documentation,GKLocalPlayer.isPersonalizedCommunicationRestricted(iOS 14.0+),2026年10月4日存下的文件 JSON,https://developer.apple.com/documentation/gamekit/gklocalplayer/ispersonalizedcommunicationrestricted。為了引用而攤平,方式同註 18。遊戲自己的預設台詞屬於頁面要求遊戲停用的「custom communication features」(自訂溝通功能),而表情不屬於,這兩點都是作者的解讀。 ↩↩↩↩↩↩↩↩

  52. Apple Developer Documentation,GKLocalPlayer.isUnderage(iOS 4.1+),2026年10月4日存下的文件 JSON,https://developer.apple.com/documentation/gamekit/gklocalplayer/isunderage。為了引用而攤平,方式同註 18。 ↩↩↩↩

  53. Apple Support,「Set up parental controls to manage your child’s iPhone or iPad」,「Set restrictions for Game Center」一節,2026年10月4日存下,https://support.apple.com/en-us/105121。 ↩

  54. Apple Newsroom,「Apple expands tools to help parents protect kids and teens online」,2025年6月11日,2026年10月4日存下,https://www.apple.com/newsroom/2025/06/apple-expands-tools-to-help-parents-protect-kids-and-teens-online/。 ↩↩↩

  55. Apple,「App Review Guidelines」,最後更新於2026年6月8日,準則 1.2、1.3 與 5.1.4,2026年10月4日存下,https://developer.apple.com/app-store/review/guidelines/。 ↩↩↩↩↩↩↩↩

  56. Federal Trade Commission,「FTC Finalizes Changes to Children’s Privacy Rule Limiting Companies’ Ability to Monetize Kids’ Data」,新聞稿,2025年1月16日,2026年10月4日存下,https://www.ftc.gov/news-events/news/press-releases/2025/01/ftc-finalizes-changes-childrens-privacy-rule-limiting-companies-ability-monetize-kids-data。 ↩

  57. 作者對 Kiradex 伺服器啟動指令及其所執行之網頁伺服器的閱讀,2026年10月5日,唯讀:server/Procfile(SHA-256 2e883a7e)與 server/railway.toml(17f8a3ba)以 --proxy-headers --forwarded-allow-ips='*' 啟動 uvicorn app.main:app,沒有任何日誌選項;server/requirements.txt(71c30c3b)鎖定 uvicorn[standard]==0.37.0。在伺服器環境中安裝的這個版本裡,uvicorn/config.py(379e9baf)把 access_log 預設為 true;uvicorn/protocols/http/httptools_impl.py(b6e4010a)與 h11_impl.py(e1bf8ab3)在它為 true 時,對每個請求記錄 get_client_addr(self.scope);而 uvicorn/protocols/websockets/websockets_impl.py(029977b2)在 uvicorn.error logger 上以 INFO 層級記錄帶有用戶端位址的 '%s - "WebSocket %s" [accepted]',--log-level warning 會讓它靜音。這是對程式碼的閱讀;沒有執行任何伺服器,也沒有看到任何已部署的日誌。 ↩↩

  58. Blake Crosley,「Pixel-Art People: Characters and a Creator on iPhone」,blakecrosley.com,2026年10月3日,第 4 節(PEGI 的規則)以及第 6 與第 7 節(造型字串、清單,以及繪製每一個造型的工坊),https://blakecrosley.com/blog/pixel-art-characters-on-iphone。 ↩↩↩

  59. PEGI,「What do the labels mean?」,2026年10月3日存取,https://pegi.info/what-do-the-labels-mean,引自角色篇(註 58);本文未重新取得。 ↩↩↩

相關文章

Pixel-Art Structures: Houses, Halls and Interiors on iPhone

How Pokémon, the SNES RPGs and Stardew build houses, interiors and floors, measured from source, and how Kiradex's town …

80 分鐘閱讀

像素藝術的道路:iPhone 上的地圖連接與街區

寶可夢、星露谷物語與動物森友會如何打造城鎮之間的土地:從原始碼實測道路、接縫、關口與地圖畫面,再為 Kiradex 寫出一份規格書。

77 分鐘閱讀

為 iPhone Duo 準備您的 App:一個完整的實作範例

為 iPhone Duo 準備並送審 App:決定版面的 SDK 版本標記、一款真實 App 逐一走過每種姿態、兩塊螢幕的截圖,以及 App Store 的規則。

37 分鐘閱讀