像素艺术的光:昼与夜、天气与阴影
《宝可梦 水晶》并不另画一座夜晚的小镇。它在时钟上把一天分成三段,早晨 6 小时、白天 8 小时、夜晚 10 小时,到点时在一帧之内重新载入调色板,没有任何渐变:在调色板文件的各个区块里,七个世界调色板保留白天亮度的 0.41 到 0.68,平均 0.51(按小镇用自己的水色和屋顶色显示时,各地图组的平均值为 0.49 到 0.54),并且变蓝;只有一个调色板 YELLOW 把最亮的颜色保持在 0.90,而人物的调色板在屏幕上毫无变化。1234 《绿宝石》没有用于光照的时钟。它有 18 个天气值,下雨时用一秒把景物的调色板压到原值的约 13/16,而玩家和镇民的调色板保持原样;《绿宝石》和《火红》的室外地图没有一张在头部写着雨,《绿宝石》的雨来自少数地图上的触发器和脚本,其中两张是带有四天周期日历的道路。567 它的阴影无论尺寸大小都是单一色调、五个像素行高,它的黑暗是一个被照亮的圆,不用闪光时半径 1.5 格,用了闪光是 4.5 格。85 我的小镇 Kiradex World 已经有自己的四个时段;在 Mac 上读源码可知(这里没有任何东西是在 iPhone 上测的),它的夜晚保留图集白天亮度的 0.588,亮着的窗户为 0.901,而经过色调处理后最亮的像素只有 0.562,变化是每分钟读取一次的硬切。9 它没有天气,行走者随地面一起变暗,也没有一座建筑在地面上投下阴影。所以简报保留致敬篇定下的四个时段,用 30 分钟渐变光照,把行走者向白色提亮一半,每个区域每天掷一次天气,雨按《绿宝石》的密度和暗度来做,把 33 格建筑阴影画进地面,拒绝真实的本地天气、随太阳移动的阴影和任何闪电闪光,并给每一项配一条可以由单元测试、UI 测试、锻炉检查、脚本或截屏执行的检查,只点名应用现有的东西或简报提议新增的东西。101112
TL;DR
- 一天是一组调色板,而不是第二座小镇。 《水晶》在 4:00、10:00 和 18:00 把一天分成 6、8、10 小时,背景调色板有五个区块(早晨、白天、夜晚、黑暗、室内);夜晚时,在这些区块里每个世界调色板平均保留白天亮度的 0.511(按小镇用自己的水色和屋顶色显示时为 0.509),蓝色以 5 位单位上升 6.44(显示时为 7.27);早晨只改变 32 种颜色中的 5 种,全都是最亮的那一种。12134
- 亮着的东西拒绝黑暗,人物保持本色。 《水晶》的 YELLOW 在夜晚和黑暗中仍把最亮的颜色保持在亮度 0.90,而其他所有世界颜色都低于 0.6;它的五个人物调色板只改变透明色。《绿宝石》的雨让 16 个背景调色板中的 14 个变暗,只把玩家和五个镇民槽位的调色板严格留在恒等映射上。114515
- 整点硬切,天气渐变。 《水晶》在整点变化的那一帧替换调色板,把 8 帧的白色渐变留给门、传送、飞翔、菜单打开的画面和一些脚本场景(它的黑色渐变只有十一个地图脚本调用;白色渐变还有 16 个地图脚本调用);《绿宝石》用 3 步、每步 20 帧、共 1.00 秒渐入雨天;《星露谷物语》的夜晚“gradually becomes darker over time”(随时间逐渐变暗)。1516
- 天气属于地点和日子。 《绿宝石》(68 张地图)和《火红》(76 张)的室外地图没有一张头部是雨;《绿宝石》的雨来自少数地图上的触发器和脚本(5 张地图上的 86 个坐标天气事件中,4 个是 RAIN,24 个启动两条道路的周期;24 次脚本天气调用中,1 次是 RAIN,2 次是暴雨),Route 119 和 Route 123 按四天周期每天推进一档。《星露谷物语》按季节每天掷一次天气,并在前一天预报。571718
- 雨稀而快,闪电短而少。 一屏 10 滴,每秒 868 像素,偏离竖直 26.6 度,每滴 18 帧,外加 7 帧的水花;闪电持续 6 到 8 帧,之间至少隔 61 个暗帧,周期之间相隔 6 到 12 秒;游戏中途转为下雨时,雨滴在变化后约两秒到位,调色板要早一秒左右。54
- Kiradex 的一天能用,但会硬切。 它的四个时段(3、8、3、10 小时)让夜晚的图集降到白天亮度的 0.588,不受色调影响的灯光网格为 0.901,加法光池只加亮、从不变暗;但光照是每 60 秒读取一次的硬切,行走者会染上夜晚色调,
Air里没有任何地方读取 Reduce Motion(减弱动态效果),建筑也不在地面投下阴影。这些全都是在 Mac 上从源码和导出的图像读出来的,不是在 iPhone 上测的。91920 - 这能给 Kiradex 什么。 一份简报:保留致敬篇的四个时段,按时钟计算的 30 分钟渐变(以 0.0036 的步长代替 0.108 的硬切),行走者向白色提亮一半(夜晚亮度 0.796,而场所为 0.593),每个区域每天掷一次的五种天气状态,每个视图 15 滴、压暗到 13/16 并用一秒渐变的雨,每秒一个图块的云影,烘焙进去的 33 格建筑阴影,窗户显示室外时段的房间,Reduce Motion 模式,没有闪电闪光,以及十一项检查。其中的 Kiradex 数字是在 Mac 上根据应用源码和导出图像计算的,不是在 iPhone 上测的。101112
Kiradex 是我为 iPhone 做的卡牌收藏应用,Kiradex World 是其中的一座像素小镇,收藏者们在那里散步、见面、展示卡牌。小镇已经有了一天。Daylight.swift 从手机读取钟点并命名四个时段;绑定层把所有不发光的东西乘以该时段的色调;亮着的窗户和大厅的壁灯是第二套不加色调绘制的网格;从第 29 版构建起,路灯和窗户会在地面投下加法光池,白天有一层云片飘过小镇,早晨公园里聚着雾,秋天落叶,夜里萤火虫游荡;还有六根烟囱冒烟,设计计划把它放在第 28 版构建。212022239 负责空气的文件用一行话说明了它的用途:“none of it takes the day’s tint: light is light.”(这些都不受当天色调影响:光就是光。)22
小镇缺少的是天气、任何人都会称为渐进的光照变化、让人物在夜里依然清晰可辨的理由,以及地面上哪怕一道建筑阴影。拼装建筑的锻炉在自己的 docstring 里把最后这一项推迟了:“The ground shadow stays off the building: it belongs to the ground layer.”(地面阴影不放在建筑上:它属于地面层。)24 所以这篇文章对光、天气和时间所做的,就是本系列前几篇对建筑和运动所做的事。它测量《水晶》如何保持一天(钟点、调色板、变化如何呈现、发生在哪些地图上),《绿宝石》如何处理天气(常量、压暗、雨、雾、沙、云和雷,以及各自在哪里发生),两者如何处理黑暗和阴影,《星露谷物语》的 Wiki 对它的一天和天气记录了什么,以及现代带光照的像素游戏的制作者如何谈论自己的光。然后它从源码读取 Kiradex 自己的光照,把简报写成相对于已发布版本的差异。
测量方法
宝可梦的数字来自 pret 反编译项目的浅克隆,这些社区项目用命名了常量的 C 和汇编重建每一款游戏:pokecrystal 在 5beda23(2026 年 9 月 29 日提交),pokeemerald 在 731ad5b(10 月 1 日),pokefirered 在 037335f(9 月 26 日),pokered 在 d2704a6(9 月 22 日)。25 四个 Python 脚本测量它们和 Kiradex,第五个把经典作品的数字换算成 Kiradex 的单位,第六个读取保存下来的 Apple 文档页面;每个脚本都连同输出保存在作者为本文准备的证据文件夹里,几秒钟就能重跑。发布前我把它们全部重跑了一遍。两个逐字节返回了保存的输出,四个在过程中做了修正,每个的早期版本和输出都保留在旁边:提议脚本改为逐行放置建筑阴影,按第 10 节的定义统计天气的潮湿期,并从应用的 Motion.swift 读取 Kiradex 的步行速度,而不是沿用每秒 4 个图块(从源码读出,应用的步行是每秒 60 像素,第 2、7、10 节用的是这个值);《绿宝石》脚本的雨标签曾把生成计数器的上限当作生成周期打印出来(第 2 节用的是修正后的数字);《水晶》脚本新增了对四种渐变各由谁调用的普查,它的地图头解析器原先只接受单词字段,因而漏掉了音乐字段是表达式的五层广播塔(头部是 388 个而不是 383 个,第 1 节使用了所有变动过的计数),现在它还会根据 environment_colors.asm 和屋顶表解析出地图实际显示的调色板组,并统计哪些画面经过菜单渐变、哪些室内地图使用自己的调色板表;Kiradex 脚本的云那一行曾把同样的每秒 4 个图块作为标签沿用。其余部分的输出与之前相同。1215891126 第七个脚本在纸面上重放简报中两次截屏时镜头停在哪里,第八个 arith.py 打印正文用到的每个派生数字,从帧时长到夜昼之比,第九个把应用的颜色常量与《水晶》的调色板比较,第十个从源码的状态机逐次调用地重放《绿宝石》的雨天设置和雷暴周期,第十一个统计收藏者的帧中有多少被收藏者自身覆盖,并为简报的检查 3 计算每种站立画面、以及渲染器处理色调的每种方式下的夜间比值;第七、第九和第十个也重跑出了保存的输出。274282930
这里的明暗用的是亮度(luma):对缩放到 0 到 1 的存储通道值应用 Rec. 601 权重(红 0.299、绿 0.587、蓝 0.114),不做伽马校正,也不建立屏幕模型。它是比较调色板的阅读辅助,不是色度学,而且 Game Boy Color 的颜色是存储值,不是它的 LCD 实际显示的样子。121 格指掌机的 16×16 像素地图方格,图块指 Kiradex 边长 16 纹素的方格,所以两者可以一一对比。按掌机速率换算的秒数使用每秒 59.7275 帧,Game Boy Advance 为 2^24 ÷ 280,896,Game Boy Color 为 2^22 ÷ 70,224,速率相同。124
整篇文章都有一个限制。其中没有任何东西是在 iPhone 上测量的。每个 Kiradex 数字要么是把应用源码当作文本读出来的,要么是在 Mac 上根据它导出的图片计算的:用脚本把色调应用到已发布的小镇图集上、云图的 alpha、小镇的导出数据。正文给出 Kiradex 的亮度时,指的就是这种计算,它假设绑定层的乘法色调像 SpriteSheet.material 使用 UnlitMaterial 的 tint 那样逐通道作用于纹理;凡是设备上的开销会产生影响的地方,都标注为未测量。129
1. 从源码实测《水晶》的一天
《金》《银》《水晶》用实时时钟计时,本文读的是三者中的《水晶》。2 本节所有内容都来自 measure_gen2_daylight.py,它读取时钟的常量和表、随钟点变化的三个调色板文件、应用它们的例程、说明每种环境的地图显示文件中哪些调色板的表,以及说明哪些地图跟随时钟的 388 个地图头。1
一天分三段
常量是 MORN_HOUR 4、DAY_HOUR 10 和 NITE_HOUR 18,把钟点换算成一天中某一段的 TimesOfDay 表在注释里写着自己的摘要:“0400-0959 morn | 1000-1759 day | 1800-0359 nite”。21 也就是早晨 6 小时、白天 8 小时、夜晚 10 小时,夜晚最长,占一天的 41.7%。14 本系列的致敬篇读过同样的常量和同样的注释,两者一致。10
没有天空的地方还有第四种状态。保存一天时段的变量 wTimeOfDay 取值 MORN、DAY、NITE 和 DARKNESS,背景调色板文件 bg_tiles.pal 有五个各含八个调色板的区块,早晨、白天、夜晚、黑暗和室内各一个,另外还有一对用于室外水面。31131 一个区块里的八个调色板,就是城都每个图块都会用到的八个具名调色板:GRAY、RED、GREEN、WATER、YELLOW、BROWN、ROOF 和 TEXT。132
同一个钟面上的三种一天,取自《水晶》的常量、Kiradex 的 Daylight.swift 和 Stardew Valley Wiki;由 figures/make_figures.py 绘制。
夜晚只剩一半的光,并且偏蓝
是亮度,不是颜色:各调色板四种颜色按时段计算的平均亮度,文件的基础区块以及旁边小镇显示的水取自 gen2_daylight.txt;Kiradex 的图集取自 kiradex_light.txt,是在 Mac 上根据已发布的图集计算的,不是在 iPhone 上测的。没有复现任何调色板颜色。
在调色板文件的五个区块里,以每个调色板四种颜色的平均亮度来衡量,夜晚区块保留白天区块亮度的比例从 RED 的 0.406 到 YELLOW 的 0.684 不等,七个世界调色板的平均值为 0.511。界面调色板 TEXT 在每个钟点都完全相同。1 在黑暗区块里,世界调色板降到亮度 0.009,只有 YELLOW 为 0.224。1
这些是文件中原样存放的区块,小镇并不会原封不动地显示它们。environment_colors.asm 为每种环境、每个时段指定从文件的 42 个调色板中载入哪八个:TOWN 或 ROUTE 使用区块自己的调色板,只有 WATER 来自文件的室外那一对,早晨和白天一个、夜晚一个,然后 LoadMapPals 把地图组的两种屋顶色写到 ROOF 中间的两种颜色上。INDOOR 或 GATE 地图在早晨和白天使用文件的室内区块,除非它的图块集载入自己的表(见下文);CAVE 或 DUNGEON 原样使用各区块。33341 这样解析之后,小镇水面的平均亮度白天为 0.528、夜晚为 0.248,是白天的 0.470,而基础区块是 0.448;它的 ROOF 在含有城镇或道路的 25 个地图组中保留白天的 0.369(Blackthorn 的)到 0.688;按小镇显示的七个世界调色板,各组保留白天亮度的平均 0.494 到 0.539,25 组合计 0.509,对应区块的 0.511。14 图中画的是区块,以及旁边实际显示的水;下面的数字都会说明来自哪一组。
夜晚还会改变色相。在 YELLOW 和 TEXT 以外的世界颜色上,蓝色减去红绿平均值,在基础区块中白天为 −0.73、夜晚为 +5.71,以 Game Boy Color 的 5 位单位计上升 6.44,约为满量程 31 的五分之一;用小镇的水代替区块的水,则为 −1.17 和 +6.10,上升 7.27。14 文件里的机制一目了然:在夜晚区块中,除 YELLOW 外每个世界调色板最亮的颜色都相同,是 (15, 14, 24),一种亮度 0.498 的蓝灰色,而白天大多数是 (27, 31, 27),亮度 0.947。按小镇显示时,七个中有五个共用这种颜色,即 GRAY、RED、GREEN、BROWN 和 ROOF(屋顶表不动最亮的颜色);水面夜晚最亮的颜色是室外那一对自己的 (15, 13, 27),亮度 0.490,绿色差一档、蓝色差三档。14 按我的理解,正是因为最亮的颜色是共用的,水只差一档,夜里的《水晶》小镇才读起来像同一片天空下的同一个地方,而不是六个各自变暗的调色板。
早晨是更暖的白色
早晨不是一个独立的场景。它与白天只有 32 种世界颜色中的 5 种不同,占 15.6%,而这五种全是 GRAY、RED、YELLOW、BROWN 和 ROOF 的颜色 0,即最亮的那种:早晨是 (28, 31, 16),白天是 (27, 31, 27),蓝色通道降了 11。14 每个调色板中更暗的颜色都和白天一样。经典作品里的早晨,就是把白色调暖的日光,其余什么都不动。
有一个调色板拒绝黑暗
在夜晚和黑暗中,YELLOW 的颜色 0 是 (30, 30, 11),亮度 0.90。无论在哪个区块,还是在小镇显示的那一组里,它都是唯一高于亮度 0.6 的世界颜色;其他调色板最亮的颜色在夜晚为 0.498(小镇的水为 0.490),在黑暗中接近黑色。14 在城都图块集中,johto_palette_map.asm 只把 YELLOW 分配给 192 个图块中的 3 个,占 1.6%:第 0 组的图块 38、55 和 56。GRAY 有 36 个,BROWN 99 个,RED 26 个,GREEN 12 个,ROOF 9 个,WATER 7 个。3514
为了看清这三个图块是什么,脚本把它们放大四倍渲染出来;渲染图源自受版权保护的美术,仅留作分析,不予发布。1 在我看来,图块 38 像一扇有两块玻璃的窗户。这是我对一张图的解读,不是源码里的标签,源码只标出了调色板。如果我的理解没错,《水晶》的灯是一种颜色而不是一束光:游戏把少数夜里应该发光的图块标上唯一那个最亮颜色不会下降的调色板,于是周围的小镇变蓝时,窗户依然亮着。
亮着的东西保持明亮:《水晶》的 YELLOW 与它的其他夜间颜色,以及 Kiradex 的发光格和光池与它染色后的小镇;取自 gen2_daylight.txt、kiradex_light.txt 和 arith.txt。Kiradex 的数值是在 Mac 上根据导出的图片计算的,不是在 iPhone 上测的。
人物不会变暗
npc_sprites.pal 中的对象调色板也跟随一天的时段,但方式与背景不同。五个人物调色板(红、蓝、绿、棕、粉)在白天与夜晚或黑暗之间只改变颜色 0,而对象调色板的颜色 0 在 Game Boy Color 上是透明的,所以它们在屏幕上根本不变。SILVER 的任何颜色都不变。只有 TREE 和 ROCK 这两个属于景物的对象调色板四种颜色全变。141 《水晶》的夜里,小镇变蓝,而走在其中的人画得与正午一模一样。
屋顶有自己的夜晚
屋顶有一张自己的表。roofs.pal 有 27 个条目,第一个未使用,每个地图组一个,每个条目有一对用于早晨和白天的颜色,一对用于夜晚。在各组之间,夜间那一对的亮度与白天那一对之比从 0.247 到 1.000,平均 0.556。361 本系列的建筑篇介绍过同一张表,以及一个图块集如何让城都的每座城镇都有自己的颜色。23
重新载入了什么
LoadMapPals 在地图载入或时段变化时应用这一切。它复制对应时段的八个背景调色板,每个 8 字节,也就是 environment_colors.asm 为该地图的环境指定的那八个;但六个室内和洞穴图块集例外(TILESET_POKECOM_CENTER、TILESET_BATTLE_TOWER_INSIDE、殿堂之外的 TILESET_ICE_PATH、TILESET_HOUSE、TILESET_RADIO_TOWER 和 TILESET_MANSION),它们的背景调色板由 LoadSpecialMapPalette 先从自己的表里载入,所以这次复制被跳过;然后在每张地图上复制对应时段的八个对象调色板;最后,仅在 TOWN 或 ROUTE 中,把地图组的两种屋顶色共 4 字节写入 ROOF 调色板。341 没有哪张 TOWN 或 ROUTE 地图使用这六个图块集(使用它们的是 75 张 INDOOR 地图和 5 张 CAVE 地图,其中除了位于 TILESET_ICE_PATH 上的殿堂之外,所有 INDOOR 地图都使用自己的表,共 74 张),所以在室外是 64 字节背景调色板、64 字节对象调色板和 4 字节屋顶,共 132 字节。14
这修正了本系列里的一句话。关于世界的第一篇写道:“time of day in Johto is a 64-byte palette reload”(城都的昼夜是一次 64 字节的调色板重载)。32 就背景调色板而言这没错。对照 LoadMapPals,这次重载还复制了 64 字节的对象调色板,在室外还有 4 字节的屋顶色;人物调色板之后看起来一样,因为只有它们的透明色变了,但这些字节确实被重新载入了。341 这是本文的发现。我没有修改之前那篇。
变化是硬切
只要地图事件处于开启状态,大地图循环每一轮都会调用 UpdateTime、GetJoypad 和 TimeOfDayPals;当 wMapEventStatus 为 MAPEVENTS_OFF 时,HandleMapTimeAndJoypad 一开始就返回。371 当强制调色板组生效(FORCED_PALSET_F)或时段没有变化时,_TimeOfDayPals 立即返回。时段变化时,它替换调色板组,调用一次 _UpdateTimePals,即 ld c, $9 ; normal 后接 DmgToCgbTimePals,然后等待一帧。早晨与白天、白天与夜晚、夜晚与早晨之间都没有分步渐变:调色板组在一帧之内改变。31
游戏确实有渐变,只是留作别用。FadeInFromBlack、FadeOutToBlack、FadeInFromWhite 和 FadeOutToWhite 各自以每步 2 帧走过 4 个调色板,共 8 帧,134 毫秒。314 地图设置脚本用的是白色那一对。玩家踩上的传送点,也就是门或洞口,会用 Door 脚本载入地图,它先渐出到白色,再落入 Train 脚本,从白色渐入;脚本传送运行的 Warp 脚本在结束前两条命令处从白色渐入;飞翔的脚本渐出到白色后落入 Warp 脚本;通过 FadeToMenu 打开、通过 FinishExitMenu 返回的画面会渐出到白色再回来:开始菜单中有六项是这样(StartMenu_Pokedex、StartMenu_Pokemon、StartMenu_Pack、StartMenu_Status、StartMenu_Option 和 StartMenu_Pokegear),另外七个文件中还有 15 次调用,其中包括商店、电脑和 Select 键道具的调用。开始菜单本身没有渐变:StartMenu 直接画在地图上,在打开和关闭时调用 UpdateTimePals,应用当前时段的调色板。没有哪个设置脚本点名黑色那一对。只有十一个地图脚本通过 special 用到它,每个都有一次渐出和一次渐回,其中包括 Sprout Tower 的三楼、Route 24 和火箭队基地的地下二层。381 地图脚本也调用白色那一对,而且更频繁:在 16 个地图脚本中,通过 special 有 27 次渐出到白色和 12 次渐入,其中包括没有人去任何地方的场景,比如 Daisy 在 Blue 家里给宝可梦梳毛,以及灯塔六楼的那些场景;黑色那一对的十一处之一,Route 26 的休息屋,是休息和回复,而不是出行。1 所以,普查所显示的经典作品语法是:渐变标记场景转换、出行、菜单或脚本时刻,而整点不属于其中任何一种。
玩家有没有看到硬切,取决于他们在 4:00、10:00 或 18:00 身在何处。如果在跟随时钟的地图的室外,硬切就在那一帧出现在屏幕上:大地图循环在玩家行走时应用调色板。在室内,变化的调色板并不是当时显示的那一个。在通过 FadeToMenu 打开的画面里,返回时会在从白色渐入之前重载时段调色板,所以新的一组是在渐变的遮掩下到来的;在开始菜单本身中,UpdateTimePals 在菜单关闭时应用它。37381 这些情况各自多常发生,源码无从回答。一部被人放在广场上一直开着的手机是另一种情形,第 7 节会再谈到。
只有室外跟随时间
每个地图头都指定一种环境和一种调色板模式。在 388 个头中,全部 23 张 TOWN 地图和全部 54 张 ROUTE 地图都是跟随时钟的 PALETTE_AUTO 模式,另有一张 CAVE 地图也是:388 张中的 78 张,占 20.1%。3914 室内时钟停止。208 张 INDOOR 地图中的 207 张和 25 张 GATE 地图中的 24 张是 PALETTE_DAY,无论几点都固定为日光。其中 133 张 INDOOR 地图显示的日光是文件的室内区块;另外 74 张位于 HOUSE、RADIO_TOWER、MANSION、BATTLE_TOWER_INSIDE 和 POKECOM_CENTER 图块集上,显示的是 LoadSpecialMapPalette 为这些图块集载入的表(见上文“重新载入了什么”)。13334 13 张地图是 PALETTE_DARK:Whirl Islands 的八张地图、Silver Cave 的第一个房间、Dark Cave 的两个入口和 Rock Tunnel 的两层。27 张是 PALETTE_NITE,永远是夜晚。其中 24 张是 CAVE 地图,包括 Ilex Forest 和 Tohjo Falls;另外三张是 Burned Tower 一楼(DUNGEON 地图)、Dragon Shrine(INDOOR)和 Underground Path(GATE)。1394
所以在《水晶》里,昼夜是露天的属性。晚上 22:00 的房子和正午的房子一样亮,树冠下的森林永远是夜晚,而黑暗又是第三种东西。1
闪光是一个调色板,《红》的黑暗是一个偏移量
《水晶》的闪光不画圆。在 PALETTE_DARK 地图上,ReplaceTimeOfDayPals 使用 DARKNESS 组,除非设置了状态标志 STATUSFLAGS_FLASH_F,这时它在每个时段都使用 NITE 组。3 用闪光照亮洞穴,会让洞穴从黑色变成夜色,也就是 18:00 之后小镇所穿的那套蓝色调色板。
上一代的《红》只有一个黑暗的地方,用一行代码处理。当传送的目的地是 ROCK_TUNNEL_1F 时,传送代码把 wMapPalOffset 设为 6,LoadGBPal 从 FadePal4 中减去这个偏移量,落到 FadePal2,它的背景条目是 dc 3,3,3,2:几乎全黑。4041 这次从室外地图出发的传送,是 pokered 把这个偏移量设成 0 或战斗保存后又恢复的值以外任何值的唯一地方。两张室内地图之间的传送会让它保持原样,而下一层 ROCK_TUNNEL_B1F 有四个传送点,全都通向一楼,所以只能从一楼到达,并同样保持黑暗;闪光、全灭、飞翔或迷宫传送,以及返回室外的传送,都会把它设为 0。4042 《红》没有时钟,Rock Tunnel 的两层是它唯一黑暗的地方。
2. 从源码实测《绿宝石》的天气
没有用于光照的时钟
Game Boy Advance 上的作品走了另一条路。《绿宝石》和《火红》都没有用于光照的昼夜系统:两份反编译中,src/*.c 或 include/**/*.h 里没有任何文件匹配“timeofday”或“time_of_day”。5 《绿宝石》确实有实时时钟,致敬篇测量过它驱动的东西:按现实时间生长的树果,以及日期变化时运行的十一项每日更新,天气也在其中(鲜活小镇篇点出了那个函数,UpdateWeatherPerDay)。1043 它们都不会按钟点去动调色板。《水晶》把颜色花在时间上的地方,《绿宝石》把颜色花在了天空上。
十八种天气,三类
include/constants/weather.h 命名了 18 个 WEATHER_* 值:NONE、SUNNY_CLOUDS、SUNNY、RAIN、SNOW、RAIN_THUNDERSTORM、FOG_HORIZONTAL、VOLCANIC_ASH、SANDSTORM、FOG_DIAGONAL、UNDERWATER、SHADE、DROUGHT、DOWNPOUR、UNDERWATER_BUBBLES、ABNORMAL,以及两个道路周期。头文件把其中三个标为“// Unused”(未使用)(SNOW、FOG_DIAGONAL 和 UNDERWATER),SHADE 还有一条自己的注释:“Original name was closer to WEATHER_CLOUDY/OVERCAST”(原名更接近 WEATHER_CLOUDY/OVERCAST)。另有 13 个 COORD_EVENT_WEATHER_* 值供地图触发器使用。445
- 压暗调色板的天气。 四种在用的天气 RAIN、RAIN_THUNDERSTORM、DOWNPOUR 和 SHADE 把调色板带到颜色映射 3;未使用的 SNOW 也是如此。每种都在自己的
InitVars里把targetColorMapIndex设为 3,colorMapStepDelay设为 20 帧。5 - 有自己表格的天气。 DROUGHT 在
src/field_weather.c里使用自己预先计算的颜色表,通过负的映射索引访问,渐入后为 −6。612 - 不动调色板的天气。 SUNNY_CLOUDS、FOG_HORIZONTAL、FOG_DIAGONAL、VOLCANIC_ASH 和 SANDSTORM 保持颜色映射 0,改为用硬件的 alpha 混合在场景上叠一层精灵,逐通道计算
(eva × target1 + evb × target2) / 16:云趋向 12 和 8,水平雾趋向 12 和 8、之后是 4 和 16,火山灰和沙是 16 和 0。5
雨把场所压暗到 13/16
压暗来自 BuildColorMaps,脚本用 Python 重放了它。它构建 19 张映射,每张都是把一个 5 位通道值换成另一个值的表。前三张每一步减去该值除以 16 的结果,所以雨和阴天使用的映射 3 把通道值 v 映射为约 0.81 v:31 变成 25,16 变成 13,8 变成 6。在 1 到 31 的取值上,输出与输入之比从不超过 0.8125,在 16 处达到。65 雨用 3 步、每步 20 帧,共 60 帧,1.00 秒到达那里。54
于是 13/16,即 0.8125,就成了经典作品里雨的数字:每个被压暗的调色板的每个通道约为五分之四。相比保留一半光线的《水晶》夜晚,这是一种温和的压暗。14
《绿宝石》的雨和闪电映射,由 figures/make_figures.py 从 BuildColorMaps 重放,并断言 measure_gen3_weather.py 打印出的各点。是曲线,不是美术。
雨不碰人物
哪些调色板使用压暗的映射也由一张表决定,即 sBasePaletteColorMapTypes,32 个条目,每个硬件调色板一个。6 背景调色板 0 到 13 使用压暗映射;14 和 15 是界面,不使用任何映射。16 个对象调色板中,10 个使用压暗映射,6 个使用普通的“contrast”映射,它的映射 3 是恒等映射:画的是什么,就保持什么。5
对照 include/event_object_movement.h 中命名的调色板槽位,这六个是对象调色板 0、2、3、4、5 和 10,即 PALSLOT_PLAYER、PALSLOT_NPC_1 到 PALSLOT_NPC_4 以及 PALSLOT_NPC_SPECIAL:玩家和五个镇民槽位。每个倒影槽位,无论是玩家的还是每个镇民的,都使用压暗映射。155 在雨中,《绿宝石》的地面、建筑、水,甚至水面上的倒影都降到 13/16,而走在其中的人保持自己的颜色。
两代游戏,两种机制,一条规则。《水晶》的人物调色板在夜晚只改变透明色;《绿宝石》的人物调色板在雨中停留在恒等映射上。光属于场所,而人物在其中保持清晰。15 源码只说明了映射,没有说明原因。按我的理解,原因是在小而暗的屏幕上保持可读性,在那样的屏幕上,被压暗的精灵最先失去轮廓;无论如何,效果是:人物永远不会成为需要眯眼去看的那个东西。
闪电是同一张表的另一端
同一组表的另一端就是闪光。最后一张映射 19 把 12 及以上的每个通道值送到 31,把小于 12 的值留在原处附近:0 变成 1,4 仍是 4,8 仍是 8,11 变成 10。56 这是一种高对比度的闪光,而不是一块白屏:每个调色板亮的一半变白,暗的一半依然是暗的。
实测雨
雨是 10 个雨滴精灵;雷暴是 16 个,暴雨是 24 个,这也是上限。每个雨滴是一个 16×32 的精灵。当 ++rainSpriteVisibleCounter > rainSpriteVisibleDelay 时,UpdateVisibleRainSprites 激活一滴雨,这个计数器在延迟之后的那一帧触发:雨的延迟是 8,两种更重的是 4,所以雨每 9 帧加入一滴新雨,雷暴或暴雨每 5 帧一滴,直到数量达标。仅这个激活阶段,从零个激活雨滴算起,雨就要 90 帧,1.51 秒,雷暴 80 帧,1.34 秒,暴雨 120 帧,2.01 秒。754 这还不是全部变化。从状态机逐次调用重放可知,游戏中途从晴空(SUNNY,它的收尾函数立即返回)切换天气,开始于 Task_WeatherMain 替换天气的那一帧;从下一帧起,Rain_Main 在第一次调用时载入雨滴图集,在接下来的 24 次调用中每次创建一个、共 24 个雨滴精灵,然后才开始激活它们,所以雨的最后一滴在替换后 115 帧,即 1.93 秒时激活;雷暴是 105 帧,1.76 秒;暴雨是 145 帧,2.43 秒。调色板先完成:三步各 20 帧,在第 59 帧到达映射 3,早于任何一种的最后一滴,而雷暴的第一道闪电可以在第 109 帧落下。在两种雨之间切换时,精灵保留,只改变数量,每 5 或 9 帧一滴:从雨转为雷暴时,到第 32 次调用为止激活多出的六滴。如果载入地图时已经在下雨,Rain_InitAll 会在一帧之内循环执行同样的步骤,雨是 116 次调用,屏幕一开始就是满的。雨停时,同样的调用每 9 或 5 帧收回一滴。29764
每滴雨以 Q28.4 定点数存储的固定速度移动,16 代表一个像素。雨以每帧 (−6.5, +13.0) 像素下落,即每帧 14.5 像素、每秒 868 像素,偏离竖直 26.6 度,持续 18 帧、262 像素,然后溅开。暴雨的雨滴以每帧 (−10, +20) 像素下落,每帧 22.4、每秒 1,336,角度相同,持续 12 帧、268 像素。57 两者落地前都要下落约 16 格的距离,分别用 0.301 秒和 0.201 秒。4 水花动画是 3 + 2 + 2 = 7 帧,0.117 秒;重水花是 3 + 3 + 4 = 10 帧,0.167 秒。54
所以经典作品里的雨稀而快:240×160 的屏幕上十道雨线,约五分之一秒穿过屏幕,每道从顶端到水花结束存在 25 帧,0.42 秒。4 它读起来像雨,靠的是角度、速度和水花,而不是密度。
雾、沙和云
- 水平雾每 4 帧滚动 1 像素,每秒 14.9 像素,是步行速度的四分之一,在 8/16 之上向 12/16 混合。54
- 斜向雾未使用,横向每 3 帧、向下每 5 帧移动 1 像素:每秒 19.9 和 11.9 像素。5
- 沙暴每帧把图层移动
gSineTable[index] × 4 / 256像素,索引每 6 帧从 0x20 走到 0x5F。对照src/trig.c中的正弦表重放 6,000 帧,结果是每帧 2.83 到 4.00 像素,平均向左 3.59(每秒 215 像素)、向上 0.90:是步行速度的 3.6 倍,也是唯一比玩家还快的天气图层。454 - SUNNY_CLOUDS 是三个 64×64 的云精灵,放在源码所说的“for the lower half of Route 120”(用于 Route 120 的下半部分)的地图格上,向 12 和 8 混合,由
UpdateCloudSprite移动,它的注释写着“Move 1 pixel left every 2 frames.”(每 2 帧向左移 1 像素。)即每秒 29.9 像素,是步行 59.7 的一半。7511
空气与步行之比:《绿宝石》的图层取自 gen3_weather.txt,Kiradex 如今的云和提议的云取自 kiradex_proposals.txt 和 arith.txt,Kiradex 的步行从其 Motion.swift 读出。Kiradex 的速度是在 Mac 上从源码读出的,不是在 iPhone 上测的。
雷:短、少,与闪光同时或在其后听到
雷暴运行一个小型状态机,源码把它概括为一个模式:“(SHORT_BOLT){1,2}(LONG_BOLT)?”。7 实测如下:54
- 第一个之后的每个周期都要等一个 360 到 719 帧的计时器,在计时器走完两帧后落下(下一次调用设置闪电,再下一次调用落下):从周期开始算起 361 到 720 帧,6.04 到 12.05 秒。第一个周期不等待:一旦渐入雷暴完成,
THUNDER_STATE_WAIT_CHANGE就跳过等待,直接进入THUNDER_STATE_INIT_CYCLE_1。5297 - 它会落下 1 或 2 道短闪电,每道把映射 19 保持 6 到 8 帧,0.10 到 0.13 秒,然后回到雨的映射。57
- 闪电之间的计时器是 60 到 75 帧,从一道闪电结束到下一道落下的暗间隔还要再长一帧,61 到 76 帧,1.02 到 1.27 秒;长闪电的间隔也一样。5294
- 一半的周期里会跟着一道长闪电,保持 30 到 45 帧,0.50 到 0.75 秒,之后逐渐渐回雨的映射,而不是硬切。57
- 雷声在周期最后一道闪电落下的那一帧入队:没有长闪电时,在最后一道短闪电落下时
EnqueueThunder(20),否则在长闪电落下时EnqueueThunder(100)。EnqueueThunder把计时器设为Random() % waitFrames,而每帧在Thunderstorm_Main开头调用的UpdateThunderSound将其递减,到 0 且没有其他音效在播放时就播放雷声:在计时器走完时如果没有别的声音,就是闪电后 1 到 20 帧,最多 0.33 秒,长闪电则是 1 到 100 帧,最多 1.67 秒。如果那时有其他音效在播放,UpdateThunderSound就返回并在下一帧再试,所以雷声要等那个声音结束,这些范围并不是上限。短闪电持续 6 到 8 帧,所以雷声可能在闪电还亮着时就开始。源码注明“Thunder only plays on the final bolt of the cycle.”(雷声只在周期的最后一道闪电时播放。)74
第一个之后(第一个会跳过等待)的最小等待三闪电周期,按 gen3_replay.txt 中的状态转移重放:每个间隔等于它的计时器加上下一个状态起作用所需的一帧。一秒标尺来自 arith.py。
对想照搬的人来说,这笔算术很重要。两道闪电之间的暗间隔至少 61 帧,按每秒 59.7275 帧计是 1.0213 秒,比一秒还长。所以《绿宝石》雷暴的任何一秒窗口都不会同时包含一道闪电的结束和另一道的开始,任何一秒内最多只有一次闪光。4 这低于 W3C 成功准则 2.3.1 的要求,这是一条 A 级要求,规定内容不得包含“anything that flashes more than three times in any one second period”(任何在一秒内闪烁超过三次的东西)。46 第 10 节仍然为 Kiradex 拒绝闪光,并在那里说明原因。
3. 天气在哪里:地图、触发器和日历
天气是地图的属性
每张第 3 世代地图的 map.json 都在头部指定一种天气。脚本读取全部地图,《绿宝石》518 张、《火红》425 张,并按地图类型分组。547
《绿宝石》的 68 张室外地图(9 座城市、7 座城镇、41 条道路和 11 条海上道路)中,SUNNY 出现 51 次,NONE 13 次,SHADE 2 次,FOG_HORIZONTAL 1 次,VOLCANIC_ASH 1 次。《火红》的 76 张室外地图中,SUNNY 66 次,NONE 8 次,SHADE 2 次。54 两款游戏中都有四分之三以上的室外地图默认晴天(75.0% 和 86.8%),没有一张室外地图的头部是雨。4
按地图统计的头部天气,取自 gen3_weather.txt 第 G 部分。
雨来自触发器、脚本和日历
《绿宝石》的雨是放置的,不是掷出来的。地图上有 86 个坐标天气事件,即玩家踩上去就会改变天气的格子,分布在 5 张地图上:Jagged Pass 以及 Route 113、119、120 和 123。按类型分,SUNNY 34 个,SUNNY_CLOUDS 12 个,VOLCANIC_ASH 12 个,RAIN 4 个,两个道路周期各 12 个。54 脚本另有 24 次 setweather 调用:NONE 5 次,SUNNY 4 次,ABNORMAL 3 次,DOWNPOUR 2 次,SANDSTORM 2 次(都在 Route 111),SUNNY_CLOUDS 2 次,VOLCANIC_ASH 2 次,FOG_HORIZONTAL 2 次,RAIN 1 次(Route 120),DROUGHT 1 次。5 《火红》完全没有坐标天气事件。5
有两条道路保留着日历。Route 119 循环 SUNNY、RAIN、RAIN_THUNDERSTORM、RAIN,Route 123 循环 SUNNY、SUNNY、RAIN、SUNNY,每天推进一档,由 UpdateWeatherPerDay 推进。75 Route 119 四天中有三天是湿的;Route 123 有一天。4 由于这一档每天只动一次,同一天两次走过 Route 119 的玩家两次都会遇到同样的天气,某一天的天气在任何人到达之前就已经定下了。
室内也有天气
头部字段并不只为天空而设。《绿宝石》给 2 张室内地图水平雾,给 1 张室内地图阴天;地下有 11 张地图阴天、7 张有雾。《火红》给 5 张室内地图雾、4 张阴天,给 14 张地下地图雾、1 张阴天。《绿宝石》的 14 张水下地图全都带有 UNDERWATER_BUBBLES。548 SHADE,那个注释说原名意为多云或阴天的天气,是经典作品表达房间或洞穴昏暗的方式。
这说明了天气是什么
把这些放在一起,测量结果把天气描绘成世界的一种排定状态。它属于一个地点(一条道路、一个洞穴、一个房间),随一步或一天而变,走过那条道路的人事先就知道,并且对在场的每个人都相同。5 第 5 节展示《星露谷物语》用不同的机制做同样的事:整个山谷每天掷一次,前一天公布。17 两款游戏都不显示玩家的真实天气,按我的理解,这不是硬件的限制,而是设计:世界的天气是世界本身拥有的东西,不是玩家带进来的东西。
4. 第 3 世代的黑暗与阴影
黑暗的洞穴是一个被照亮的圆
《绿宝石》把黑暗当作几何来画。src/field_screen_effect.c 中的 sFlashLevelToRadius 是 {200, 72, 64, 56, 48, 40, 32, 24, 0} 像素,从全亮到全黑共九级。495 当地图头设置了 requires_flash(地图工具把它写进 C 代码以 gMapHeader.cave 读取的那个头部字节)时,SetDefaultFlashLevel 从第 7 级开始,即 gMaxFlashLevel - 1,半径 24 像素,1.5 格;使用过闪光后则从第 1 级开始,72 像素,4.5 格。其他所有地图,无论是不是洞穴,都从第 0 级开始,全亮。505 照亮的区域是一个 Game Boy Advance 窗口,它的水平边界由 DMA 在每条扫描线上重写,以屏幕宽高的一半为中心,而不是以玩家为中心。5
在 240×160 的屏幕上,不用闪光的圆覆盖画面的 4.7%,用闪光的圆覆盖 42.4%,面积是九倍。4 从第 1 级到第 7 级,每一级把半径减少 8 像素,即半格;第 8 级是全黑。4
以格为单位的半径表示的黑暗:根据 gen3_weather.txt 打印的 sFlashLevelToRadius 画出的示意图。没有画任何地图。
《绿宝石》有七张地图设置了 requires_flash,所以 518 张中有七张一开始是黑的:其中三张是《红宝石》《蓝宝石》遗留下来的未使用地图(CaveOfOrigin_UnusedRubySapphireMap1 到 3),另外是 Granite Cave 的下面两层和 Victory Road 的两层;《火红》有两张,即 Rock Tunnel 的两层。5 与《水晶》放在一起,三代游戏用三种方式画黑暗的地方:《红》用调色板偏移量,《水晶》用一组被闪光换成夜色的调色板,《绿宝石》用一个被闪光把半径放大三倍的光圈。4035
阴影是单一色调、五行高
《绿宝石》用四个场地效果精灵绘制阴影,由 measure_shadows.py 读取。小号 8×8 像素,中号 16×8,大号 32×8,特大号 64×32。每个都只用一个颜色索引,都是位于帧底部五行中的一个扁椭圆:小号 64 个像素中 24 个不透明,中号 128 个中 64 个,大号 256 个中 144 个,特大号 2,048 个中 304 个。8514 无论行走者多大,阴影都是五行高、单一色调。本系列的台阶篇测量了这条规则的另一半:行走者跳下台阶时,阴影留在地面上。52
Kiradex 的行走者阴影 shadow.png 遵循同样的方案:12×4 像素,单一色调,(18, 17, 23),alpha 153,48 个像素中 40 个不透明,这是在 Mac 上从已发布的图片读出的,不是在 iPhone 上测的。89 alpha 153 即 60% 的覆盖,色调亮度 0.071,是一种偏蓝的近黑色。4
5. 《星露谷物语》的一天和天气
《星露谷物语》是本系列一再回头参考的现代游戏,因为它的小镇按日历运转,它的 Wiki 详细记录了一天和天气。以下是 2026 年 10 月 5 日保存的 Wiki 原文和数字;本文没有读这款游戏的代码,所以所读页面没有给出的夜晚色调值和变暗速度,这里都没有。12
一天十四分钟,不计暂停
一天从早上 6 点持续到凌晨 2 点,游戏内 20 小时,“Time passes at 0.7 real seconds per in-game minute”(游戏内每分钟相当于现实 0.7 秒),所以一整天是现实中的 840 秒,14 分钟,前提是 Wiki 自己给出的两个条件:不在骷髅洞穴里,那里一分钟要 0.9 秒(“except the Skull Cavern where it’s 0.9”),以及“Ignoring pauses”(不计暂停),因为单人模式下时钟在过场、菜单和对话时会停止。164 入夜时间随季节变化:“In Spring and Summer , it is 8pm, while in Fall it is at 7pm and Winter is 6pm.”(春夏为晚上 8 点,秋季为 7 点,冬季为 6 点。)(逗号前的空格是 Wiki 原有的。)入夜时,“indoors the windows turn dark and lamps and sconces light up”(室内窗户变暗,灯和壁灯亮起),而室外“it does not immediately become full dark, but gradually becomes darker over time.”(不会立刻全黑,而是随时间逐渐变暗。)16 从早上 6 点算起,春季的一天入夜约在现实 9.8 分钟后,冬季约在 8.4 分钟后。4
所以《星露谷物语》做了《水晶》不做的事:黑暗以渐变的方式到来,开始的时间随季节而变。它也会让室内变暗,而《水晶》把 208 张室内地图中的 207 张固定在白天;Wiki 记录了一个例外:“if nightfall happens while you are indoors it stays light until you exit and re-enter.”(如果入夜时你在室内,室内会一直亮着,直到你出去再进来。)161
灯光是地图属性
Wiki 关于地图的模组页面把光照列为地图拥有的属性。53
AmbientLight“Sets the RGB color that is subtracted from white (255,255,255)”(设置从白色 (255,255,255) 中减去的 RGB 颜色),所以地图的黑暗是减去一种颜色,而不是乘上一种颜色。53Light放置九种光源之一:灯笼、窗户、壁灯、大锅、室内窗户、投影仪、鱼缸、冬季小镇树灯和点光。53WindowLight添加“only lit during the day when it is not raining”(只在白天且不下雨时点亮)的光,也就是从外面透过窗户进来的光。53NightTiles“Changes the tile after 7pm”(晚上 7 点后更换图块),页面还补充说它“is mostly used for lamps in the game.”(在游戏中主要用于灯。)53
建筑篇把 NightTiles 用作 Kiradex 亮窗的范本。23 WindowLight 是另一半,也是 Kiradex 还没有的那一半:窗户显示外面光线的房间。
天气按天决定,前一天公布
《星露谷物语》的天气每天掷一次并公布:Wiki 的天气页面说“Weather changes daily and with the seasons”(天气每天、随季节变化),第二天的天气“can be checked by viewing the TV screen”(可以通过看电视屏幕查看),即电视的天气预报。17 对没有强制天气的日期,模组页面给出的概率是:18
- 春季:“an 18.3% base chance of rain”(下雨的基础概率为 18.3%),以及“in spring there’s a 66.4% chance of sunny weather.”(春季有 66.4% 的概率晴天。)18
- 夏季:“The chance of sunny weather diminishes steadily per day from 87.4% on summer 1 to 79.9% on summer 28”(晴天概率从夏季第 1 天的 87.4% 逐日稳步降到第 28 天的 79.9%),以及“There’s an 85% chance that rain becomes storms.”(雨有 85% 的概率变成暴风雨。)18
- 冬季:“a 63% chance of precipitation”(降水概率 63%),而关于雨本身:“Winter: never rains.”(冬季:从不下雨。)18
- **刮风(落叶):**春季“approximately 16.6% chance of debris weather”(刮风天气的概率约为 16.6%),秋季“approximately 49.8%”(约 49.8%)。18
暴风雨期间“There is a chance for a lightning strike to occur every 10 minutes”(每 10 分钟有一定概率发生雷击),游戏还允许玩家把雪调淡:“Snow transparency can be changed in the Options Menu”(雪的透明度可以在选项菜单中更改)。17 最后这一行是一个关于天气的小小无障碍设置,第 10 节采纳了这个提示。
6. 现代带光照的像素游戏,听制作者怎么说
本系列第一篇列出了像素艺术光照的分级表,这里概括一下:最底层是 Moonlighter 的叠加精灵,然后是 Celeste 的光照遮罩、《风来之国》(Eastward)手绘的凹凸贴图,最顶层是《星之海》和 HD-2D 的带光照 3D 场景;它还给 Animal Well 和 Dead Cells 定了位。32 本节回到制作者那里,看每一位说自己的光是为了什么。他们都没有给出昼夜周期或天气的数字;其中唯一的数字是 Celeste 的纹理和光源预算,也没有找到带光照数字的《风来之国》或《星之海》的说法。12
Moonlighter:根本没有光源
Digital Sun 告诉 80.lv:“There is no dynamic lighting”(没有动态光照),以及“we use the superposition of sprites to give the impression of light (for example in Rynoka during the night).”(我们用叠加精灵来营造光的印象(比如夜里的 Rynoka)。)54 它小镇里的夜晚是画出来的,不是算出来的。
Celeste:会打到墙上的光
Noel Berry 记述重做 Celeste 光照的文章写于 2017 年 9 月 25 日,开头是一项要求:简单的聚光“wouldn’t cut it”(不够用),因为“the lights needed to actually hit walls.”(光必须真的打到墙上。)他的做法是先把每个光源的聚光画进一张遮罩,然后让每个墙面在上面画出“their shadow projection (each just a single quad)”(它们的阴影投影(每个只是一个四边形)),从聚光中挖掉阴影。每个光源占用一张 2048×2048 纹理的一个颜色通道,这张纹理被分成 256×256 的槽位,共 64 个四通道槽位,所以“we can have a total of 256 lights on a single, big texture.”(我们可以在一张大纹理上总共放 256 个光源。)554
《风来之国》:一款看起来像 2D 的 3D 游戏
Pixpil 的 Tommo Zhou 在 2021 年 10 月 11 日告诉 Game Developer:“Interestingly Eastward is a 3D game with a 2D perspective!”(有趣的是,Eastward 是一款 2D 视角的 3D 游戏!)工作室使用“shader textures and lighting systems in our custom engine (Maoi) to help convey atmosphere, and things like the weather and the time”(我们自研引擎(Maoi)中的着色器纹理和光照系统,来帮助传达氛围,以及天气和时间之类的东西),有“intricate fog layers and sunlight beams breaking through the landscape during dawn and dusk”(黎明和黄昏时穿透景观的复杂雾层和阳光),它的素材在 3D 中重建,并“hand painted in the bump map one by one”(一个一个在凹凸贴图里手绘)。56
《八方旅人》与 HD-2D:阴影落在哪里
在 Nintendo Everything 于 2018 年 11 月 3 日翻译的 Nintendo Dream 访谈中,《八方旅人》(Octopath Traveler)的 Shizuka Morimoto 说:“At first, I started using some techniques to add dramatic lighting to the 2D art, but Asano told me that wasn’t enough.”(一开始我用了一些技巧给 2D 美术加上戏剧化的光照,但浅野说那还不够。)Tomoya Asano 则说:“I wanted to make use of lighting mixed with pixels in a way that emphasized how light shines on objects and how shadows fall.”(我想用一种与像素融合、能强调光如何照在物体上、阴影如何落下的光照方式。)57 作为二手资料的 Wikipedia 描述了这个系列后来为人所知的技法 HD-2D:“Typically, a point light source is placed into the scene to make characters and objects cast shadows”(通常会在场景中放置一个点光源,让角色和物体投下阴影),再叠加移轴、景深、泛光、体积光和雾。58 Asano 本人对成本的评价,出自 Nintendo Life 在 2022 年 5 月报道的 4Gamer 访谈,是文章标题下的那一行:“It costs more than you’d think”(成本比你想象的高)。59 虚幻引擎关于《八方旅人》的两个页面对抓取工具和 curl 都返回 HTTP 403,所以由 Nintendo Dream 的翻译代替。12
《星之海》:把光照当作卖点,时钟进入玩法
Sabotage 的《星之海》(Sea of Stars)媒体资料列出“Full-on dynamic lighting. Our custom-made render pipeline allows the creation of a breathtaking world coming to life by pushing the limits of 2D pixel-art games.”(全面的动态光照。我们定制的渲染管线突破 2D 像素游戏的极限,让一个令人屏息的世界活起来。)60 TechRaptor 关于一段玩法视频的报道表明,它的昼夜变化驱动玩法:它“influences gameplay as well, specifically some interesting little puzzles that materialize bridges at the right time of day.”(也影响玩法,特别是一些有趣的小谜题,会在一天中合适的时间让桥出现。)61 Sabotage Studio 的 Thierry Boulanger 在 2023 年 8 月对 Screen Rant 说,正如本系列台阶篇所引用的,它的关卡必须“to all read well with different levels of height, with the light and everything”(在不同高度上、连同光照和一切,都要读得清楚)。52
手绘的阴影
两位画师给出了完全不需要引擎的阴影规则:一位写了三篇文章,另一位说了一句话。
Raymond Schlitter 在关于俯视物件的 Pixelblog 里谈投影:“It’s best to keep them subtle and not cast out long in any particular direction”(最好保持低调,不要朝任何方向拉得很长),以及“in most cases I make the shadows on separate tiles and place them on a different layer than the sprites.”(大多数情况下,我把阴影做在单独的图块上,放在与精灵不同的图层。)62 在俯视图块文章的第二部分里,谈墙下的落影:“it’s best to limit them to one or two wall faces, minding light source, and they should not exceed a tile in length. No matter how tall the rock wall all shadows are the same length.”(最好限制在一两个墙面上,注意光源,长度不应超过一个图块。不管岩壁多高,所有阴影都一样长。)63 在俯视树木的文章里,谈树影放在哪里:“It’s most practical for game design to center the shadow directly under the tree, so that it doesn’t run into other assets”(对游戏设计来说,最实用的是把阴影正对着放在树下居中,这样它不会碰到其他素材)。64
石井浩一在 Shmuplations 翻译的访谈中谈《圣剑传说 3》:“for shadows and the like, we’re using deep blues and purples instead of shades of black, to impart a sense of softness.”(阴影之类的地方,我们用深蓝和紫色而不是各种黑色,来营造柔和感。)65
为手机重读分级表
制作者的话给分级表补充了一点。Moonlighter 以上的每一级,都是为了让光与某样东西互动:Celeste 的光打到墙上,《风来之国》的光落在凹凸贴图的表面上,《八方旅人》展示“how shadows fall.”(阴影如何落下。)555657 只做加法的光,从不被阻挡,也从不被它落在的东西塑形,不管怎样混合,都属于 Moonlighter 那一级。
Kiradex 的光池就是这样:用真正的加法混合加到地面上的光,不与任何东西互动(第 7 节)。6622 按我的理解,这不是缺陷。这是手机上的小镇负担得起的那一级,而上面那些手绘规则说明了不用引擎可以把它推进到什么程度:阴影放在自己的图层上,要短,落在一两个面上,无论高度如何都一样长,并且偏向蓝色而不是黑色。626365
7. Kiradex 如今的光照,在 Mac 上从源码读取
本节所有内容都是 2026 年 10 月 5 日 16:51 到 18:19 之间,从 Kiradex 代码库的工作树中、在提交 cf3beb1e 到 66c116b0 时读取的;当天工作树正在被编辑,读取时没有运行 git 或锻炉,每条注释都说明了是哪一次读取,读取的文件包括:Kiradex/World/Daylight.swift、Motion.swift、Air.swift、Glow.metal、PlazaStage.swift、PlazaRig.swift 和 HallView.swift,锻炉的 scripts/forge/air.py、town.py 和 buildings.py,以及导出的小镇 Kiradex/Resources/World/town.json(40×30 图块,文件日期 2026 年 10 月 3 日 21:34)及其图集和空气图片。912 measure_kiradex_light.py 把 Swift 和 Python 当作文本读取,并用 Pillow 把色调应用到已发布的图片上。其中没有任何东西在 iPhone 上运行,这里的数字也没有一个是截屏:正文说小镇的夜晚是白天的 0.588 时,指的是在 Mac 上把已发布的图集乘以已发布的色调。9
手机日历上的四个时段
Daylight.Phase 把钟点映射为:6 点到 9 点是早晨,9 点到 17 点是白天,17 点到 20 点是傍晚,其余是夜晚:3、8、3、10 小时。219 路灯在傍晚和夜晚点亮,lampsOn 为 self == .evening || self == .night,一天 13 小时,占 54.2%。214 这些并不是《水晶》的钟点,这是有意的。致敬篇把 Kiradex 的时段与《水晶》的 4、10、18 并列,决定保持 Kiradex 自己的;本文延续这一决定。10
每个时段是一种色调,即“a multiplier on the art’s own colours”(作用于美术自身颜色的乘数),文件把这四种声明为一个美术层面,“four fixed lights for a pixel world”(为像素世界准备的四种固定光)。21 连同亮度如下:9
| 时段 | 色调(红、绿、蓝) | 亮度 | 蓝色减去红绿平均值 |
|---|---|---|---|
| 早晨 | 1, 0.96, 0.86 | 0.961 | −0.12 |
| 白天 | 1, 1, 1 | 1.000 | 0.00 |
| 傍晚 | 1, 0.78, 0.6 | 0.825 | −0.29 |
| 夜晚 | 0.5, 0.58, 0.9 | 0.593 | +0.36 |
季节再乘上一层色偏:春 (0.97, 1, 0.95),亮度 0.985;夏为白色,1.000;秋 (1, 0.93, 0.84),0.941;冬 (0.9, 0.94, 1),0.935。在时段与季节的 16 种组合中,最暗的光是秋夜,(0.5, 0.539, 0.756),亮度 0.552,最亮的是夏日,1.000。921 季节按月份选择,用文件自己的话说,是“in the north; the south is a setting for later”(以北半球为准;南半球留待以后做成设置)。21
形状与经典作品一致。在《水晶》和 Kiradex 中,夜晚都是最长的时段,24 小时中占 10 小时;Kiradex 的早晨是调暖的日光,对所有颜色施加亮度 0.961 的色调,而《水晶》只调暖白色;Kiradex 的夜晚也像《水晶》一样偏蓝。194 Kiradex 增加了一个温暖、路灯点亮的傍晚,这是《水晶》没有的。
色调对小镇做了什么
应用到已发布的图集 town.png(256×352 像素,其中 81,926 个不透明)上,四种色调给出的平均亮度是:早晨 0.432,白天 0.449,傍晚 0.372,夜晚 0.264,分别是白天的 0.962、1.000、0.829 和 0.588。94 Kiradex 的夜晚比《水晶》的平均值多留一点光,0.588 对 0.511,又比夜晚色调自身的亮度 0.593 少一点,因为图集的颜色偏向夜晚色调削弱的暖色通道。4 按我的理解,对一部伸直手臂拿着的手机来说,宁可偏在这一边。
亮着的东西拒绝色调
Kiradex 走另一条路得出了《水晶》的答案。亮着的窗户和大厅的壁灯是第二套网格,材质用 light: .white 绘制,不加色调,在路灯亮起时启用;锻炉为每个发光格只导出点亮时会变化的像素。2023 在导出数据中,它们是三个图集索引中的 24 个格:20 扇窗户(索引 200,每个 54 个不透明像素),2 扇山墙窗(索引 201,同样 54 个:buildings.py 放在山墙三角形正中的窗户,在 house-mine 和大厅上;老虎窗是没有发光变体的屋顶部件),以及大厅的 2 盏壁灯(索引 257,12 个像素,位于格 (21, 5) 和 (23, 5))。9
它们最亮的像素亮度为 0.901,不加色调绘制。在夜晚色调下,图集中任何地方最亮的像素是 0.562。发光格是染上夜晚色调后图集最亮像素的 1.60 倍;这里没有算上加法光池,光池在路灯中心可达 0.785 到 0.806(见下文)。94 《水晶》的 YELLOW 为 0.90,而其他调色板夜间最亮的颜色是 0.498(小镇的水差一档,为 0.490),这是同一个想法在调色板里的体现:亮着的东西保持亮度,周围的小镇暗下去。14
这个计数修正了本系列的另一篇文章。建筑篇写的是“nineteen windows and the hall’s two sconces”(十九扇窗户和大厅的两盏壁灯),并说“21 lit cells dedupe to two”(21 个发光格去重后只剩两个)。23 10 月 3 日 21:34 的导出数据中有 24 个发光格,分属三个图集索引:20 扇窗户、2 扇山墙窗和 2 盏壁灯。94 要么那篇写完后小镇变了,要么那篇的统计方式不同;对照已发布的导出数据,它过时了三个格和一个图集索引。这是本文的发现,我没有修改之前那篇。
只做加法的光池
从第 29 版构建起,路灯和亮着的窗户在夜里会在地面投下光池。Glow.metal 是一个 RealityKit 表面着色器,它把图片颜色乘以色调同时写为基础色和自发光色,不透明度为 1,材质以加法混合;注释给出了理由:“black adds nothing, and a pool of light never darkens what it lies on, which an alpha halo did.”(黑色什么也不加,光池永远不会让它下面的东西变暗,而 alpha 光晕会。)6622 在 Air.swift 中,程序的描述符设置了 blendMode = .add。22
- 路灯光池。 56×56 像素,3.5 个图块,位于小镇 7 处光源(灯头和售货亭)各自下方,色调 (0.7, 0.52, 0.24)。922
- 窗户光池。 40×28 像素,2.5×1.75 个图块,位于亮着的底层窗户或壁灯前方,色调 (0.5, 0.38, 0.17)。只有位于建筑底行或其上一行的发光格才会投下光池,所以 24 个发光格中有 17 个会投:光池共 24 个。4
- 图片。
glow.png,64×64,一个径向光池,中心为 0.961,在 0.713 的像素上大于零。967
光池对地面的作用可以计算。夜间图集的平均颜色是 (0.228, 0.264, 0.358),亮度 0.264;在中心加上一盏路灯的完整色调,会把它提到 (0.928, 0.784, 0.598),亮度 0.806,高于白天的 0.449。9 脚本加的是完整色调;按图片自身的峰值 0.961 加权,中心为 0.785。4 无论哪种算法,夜里一盏路灯都会让它下方的地面比正午还亮,而这正是路灯对它下方地面所做的事。
行走者染上夜色
行走者并不保持本色。PlazaRig.light(_:lampsOn:) 把新的光设置到地面、周边和物件上,并把每个行走者标记为“redrawn in the new light on the next frame”(下一帧在新的光下重绘);每个行走者帧的材质是 sheet.material(column:row:light:),用的是同一个 light,与地面是同一个乘法。20 夜里,行走者以夜晚色调绘制,即三个通道乘数 (0.5, 0.58, 0.9),其自身亮度为 0.593,按存储值计算保留收藏者人物亮度(其遮罩,以 idle_a 保持)的 0.575,30 恰恰是在《水晶》和《绿宝石》不去动人物的地方。915
唯一的例外是收藏者手里拿着的卡牌。绑定层加了一个 DirectionalLight,注释是“For the card alone: nothing else in the world is lit.”(只为卡牌:世界上其他东西都不受光照。)20
这是第二处修正。世界篇的清单写着:“Dusk and night as one multiply tint over the world plus hand-drawn emissive variants for lamps, windows and the kiosk sign.”(黄昏和夜晚是覆盖整个世界的一次乘法色调,再为路灯、窗户和售货亭招牌加上手绘的自发光变体。)32 代码就是这样做的,作为对代码的描述它是准确的。本文的简报在行走者这一点上改变了它:行走者不应承受完整的色调(第 10 节,第 2 项)。对照经典作品,把一个乘法施加到包括行走者在内的一切之上,正是《水晶》和《绿宝石》都不采用的规则。这是本文的发现;世界篇未作修改。
硬切,每分钟一次
光照是 clock 的函数,那是舞台持有的一个日期。一个任务每 60 秒重读一次它,注释是“The light follows the clock, a minute at a time.”(光跟随时钟,一分钟一次。),而 onChange(of: clock) 调用 relight(),计算时段和季节,并立即分配新材质。199 光与光之间没有动画,World/ 里任何地方都没有 significantTimeChangeNotification 的观察者。钟点通过 Daylight.now 来自 Calendar.current,即手机自己的时区,启动参数 -plazaHour 可以强制一个钟点,UI 测试用它来看夜里的广场。92168 舞台确实运行着一个 TimelineView(.animation(minimumInterval: 0.1)),但那是 bubbles(of:in:) 中对话气泡的时钟,用来把刚说的话在说话人头顶保留几秒;它不是光照的时钟,也不是步行的时钟,步行由 rig.update 从场景的更新事件中推进。199
所以时段的变化是硬切,而且可能在边界后最多 60 秒才到来。以图集平均亮度计,这些硬切是(来自 arith.py):411
《水晶》也是硬切,18:00 在室外的玩家会在那一帧看到它(第 1 节);有多少人在看,源码无从回答。1 按我的理解,Kiradex 的广场是一个常开的屏幕:有人可能把它开着放在桌上,在 20:00 抬头,看到小镇的平均亮度在一帧之内从 0.372 降到 0.264,亮度少了四分之一以上。在一个有人注视的屏幕上,硬切就是一个事件,而经典作品无须解决这个问题。
房间和大厅沿用手机的时钟
PlazaStage.swift 中的 RoomView 会自己构建一个 PlazaStage,HallView 也构建一个,所以房间和大厅由与广场相同的代码、按同一个时钟,也就是手机的时钟,重新打光,路灯也在内。1969 两者都经由黑色帷幕进入,用它们自己的注释说是“A room is entered through the veil”(经由帷幕进入房间)和“The show is entered through the veil”(经由帷幕进入展会),并且都叠在广场之上,而不是替换它。1969 这是我对代码的解读,不是截屏。这是《星露谷物语》的选择,而不是《水晶》的:《星露谷物语》在入夜时让室内变暗并点亮灯,而《水晶》把 208 张室内地图中的 207 张固定在白天。161 两者都是经典作品。Kiradex 已经选了其中一个,并且又往前走了一步:《星露谷物语》在玩家入夜时身处的房间里保持明亮,直到玩家离开再回来;而 Kiradex 的房间即使玩家在里面,也会每分钟重新打光,这是 Kiradex 自己的选择,不是经典作品的。1619
空气
Air.swift 是第 29 版构建中“Light and air over a place”(场所之上的光与空气)这一层,遵循设计计划第 4 节。2270 根据代码和锻炉的图片测量:9
- 云影。 一张比任何视图都宽的片,跟随镜头,图片
cloud.png(256×256)每 384 个世界像素重复一次。它的纹理偏移每秒前进 (0.004, 0.0015),换算到 384 像素上是每秒 (1.536, 0.576) 像素,合计每秒 1.64 像素。229 这个换算假设 RealityKit 在缩放之后加上偏移,于是偏移 1 正好是图片完整重复一次。为本文保存的 Apple 页面只说这个属性是“A two-dimensional transformation to apply to the entity’s primary texture coordinates”(应用于实体主纹理坐标的二维变换),没有说顺序。71 应用自己的精灵表也依赖同样的顺序:SpriteSheet.swift用“列除以列数”的偏移和“一除以列数”的缩放来选取一帧,只有偏移在缩放之后才会落在那一帧上,它的注释还记录了在模拟器上对偏移约定的测量。72 所以 1.64,以及第 2 节图表中云的那根柱子,都依赖于这个顺序。偏移每帧还会加上镜头位置除以 384,所以云片在屏幕上的运动是漂移加上视图移动时这一项造成的效果;我没有截取它。229 除夜晚外,每个时段都显示云。 - 云荫。
cloud.png是“a tileable mask of cloud shadow: alpha where a cloud passes”(可平铺的云影遮罩:云经过之处有 alpha),覆盖其面积的 0.499,alpha 最高 110,以材质不透明度 0.55 绘制。最深的阴影把地面乘以 0.763,图片的平均值把地面乘以 0.950。679 - 雾。 公园池塘上方三团雾,位于格 (5, 24)、(9, 26) 和 (4, 27),每团 128×64 像素,alpha 最高 93,不透明度 0.7,仅在早晨出现。922
- 落叶和萤火虫。 秋天有 14 片落叶,除夜晚外的每个时段都有;夜里有 10 只萤火虫,限制在公园的矩形内。922
- 烟。 锻炉记录的 6 根烟囱上各三团烟;有一个 UI 测试等待空气的报告显示
smoke 18。2268
与经典作品相比,云很慢,阴影也很淡。每秒 1.64 像素是 Kiradex 步行的 0.027,穿过一个图块要 9.8 秒,而《绿宝石》的云以步行的一半移动;按我的理解,在手机上它慢到看起来像静止的。114 用来对比的步行速度是每秒 60 像素:Motion.swift 的刻度时钟每秒 60 个刻度,Stride.ticks 给 16 像素的直线一步分配 16 个刻度,PlazaRig.update(_:) 把刻度交给 tick(),由它推进玩家。732011 云最深的阴影 0.763 比《绿宝石》雨的映射(每个通道约 0.81)更暗,但它只覆盖图片的一半。95
阴影
有三种阴影按规则绘制(铺石路面上的灯柱也自带一种),还有一种缺失。9874
- 行走者的阴影。
shadow.png,第 4 节那个 12×4 的单色斑块,在每个行走者下方按 12×4 绘制。98 - 墙的阴影。 锻炉把墙格脚下任何东西的最上面 4 个像素行,在它自己的色阶上调暗一档;注释点明了参照:“the way the light falls at Leene.”(就像 Leene 那里光落下的样子。)74
- 屋檐的阴影。 建筑墙面上屋顶下方三个像素行的阴影,由
buildings.py在完成一座建筑时绘制。24 - 地面的阴影,缺失。 没有任何建筑在地面投下阴影。推迟它的 docstring 已在本文开头引用,锻炉的
paint从不绘制它。249
墙的阴影已经符合 Schlitter 的规则(一个面,短于一个图块,无论上面是什么都一样长),行走者的阴影符合《绿宝石》的规则(单一色调,几行高)。638 缺口在建筑。
动态与系统设置
accessibilityReduceMotion 在 World/ 中只在一个文件 FigurePreview.swift 里被读取。Air.swift 不读它,所以云的漂移、雾、落叶、萤火虫和烟,不管设置如何都会动。975 舞台确实设置了 renderingEffects.depthOfField = .disabled,尽管设计计划的第 29 版构建曾要求“a vignette and tilt-shift depth of field as the camera’s rendering effects”(以暗角和移轴景深作为镜头的渲染效果);暗角做出来了,景深是关闭的。1970
核对本系列各篇的说法
本系列前几篇中有三处说法与本文的测量不符,每一处都已在上文连同测量一起说明:建筑篇的 21 个发光格,对比导出数据的 24 个;世界篇的 64 字节重载,对比 LoadMapPals 在室外的 132 字节;以及世界篇覆盖整个世界的一次乘法色调,本简报在行走者这一点上改变了它。2332934 相符的有:致敬篇关于《水晶》和 Kiradex 的钟点;鲜活小镇篇对如今时钟的描述,即手机的日历和一个不指定时区的服务器,以及与之并列的契约,即携带房间时区和时钟的 welcome 和安静广场的响应,第 10 节以此为基础,而客户端在 10 月 5 日并没有读取它们;建筑篇对 NightTiles 和 roofs.pal 的描述;以及台阶篇里跳跃时留在地面上的阴影。1043762352 角色篇、运动篇、房间篇和道路篇没有说任何本文与之矛盾的话,运动篇里每秒 60 个刻度、每个图块 16 个刻度的步行,正是应用现在运行的。127773
8. 技艺,写成带数字的规则
下面每条规则都是第 1 到第 7 节测得的一个事实,加上我从中得出的设计解读。数字来自经典作品和应用;每个 Kiradex 数字都是在 Mac 上从源码读出或根据导出图片计算的,不是在 iPhone 上测的;解读是我的。
| 元素 | 规则 | 出处 |
|---|---|---|
| 一天的分段 | 一天分三到四段,夜晚最长。《水晶》:6 + 8 + 10 小时。Kiradex:3 + 8 + 3 + 10。 | 第 1 和第 7 节19 |
| 夜晚 | 夜晚大约只剩一半的光,并且更蓝。《水晶》的世界调色板在调色板文件的区块中保留白天亮度的 0.41 到 0.68,平均 0.51(按小镇用自己的水色和屋顶色显示时,各地图组为 0.49 到 0.54),蓝色偏向以 5 位单位上升约 6 到 7;Kiradex 的夜晚保留图集亮度的 0.59(在 Mac 上读取)。 | 第 1 和第 7 节19 |
| 早晨 | 早晨是更暖的白色,而不是另一个场景。《水晶》32 种世界颜色中有 5 种为早晨而变,全都是最亮的那一种。 | 第 1 节1 |
| 亮着的东西 | 亮着的东西拒绝色调。《水晶》的 YELLOW 夜里仍为亮度 0.90;Kiradex 的发光格不加色调,为 0.901,而夜间小镇最亮的像素是 0.562(在 Mac 上读取)。 | 第 1 和第 7 节19 |
| 人物 | 场所变暗,人物不变。《水晶》的人物调色板,以及《绿宝石》的玩家和镇民调色板,都不受夜晚和雨的影响。 | 第 1 和第 2 节15 |
| 变化 | 整点硬切,天气渐变。《水晶》:整点时一帧之内,它 8 帧的白色渐变用于门、传送、飞翔、菜单打开的画面和一些脚本场景。《绿宝石》的雨:3 步、每步 20 帧,共 1.00 秒,雨滴约一秒后到位。一部开着放在小镇画面上的手机会在硬切时被人看着,所以 Kiradex 用渐变(第 10 节)。 | 第 1、第 2 和第 7 节15 |
| 雨的光 | 雨把场所带到 13/16。《绿宝石》的映射 3,每个通道约 0.81。 | 第 2 节5 |
| 雨滴 | 雨稀而快。240×160 的屏幕上 10 滴,每秒 868 像素,偏离竖直 26.6 度,每滴 18 帧,水花 7 帧;暴雨是 24 滴,每秒 1,336 像素。 | 第 2 节5 |
| 空气 | 云和雾比你走得慢。《绿宝石》的云为步行的 0.50,雾为 0.25;只有沙暴是 3.6,更快。 | 第 2 节511 |
| 天气的排期 | 天气按地点和日子排定,事先可知。《绿宝石》和《火红》都没有头部为雨的室外地图;Route 119 和 123 有四天周期;《星露谷物语》每天掷一次,并预报第二天。 | 第 3 和第 5 节517 |
| 闪电 | 闪电短而少。每道 6 到 8 帧,最多两道短闪电,之间至少 61 个暗帧,周期之间相隔 6 到 12 秒;没有哪一秒含有两道闪电,低于 W3C 每秒三次闪光的限制。 | 第 2 节5446 |
| 室内 | 室内要么保持白天,要么跟随白天;选一种。《水晶》把 208 张室内地图中的 207 张固定在白天;《星露谷物语》在入夜时让室内变暗并点亮灯,不过入夜时玩家所在的房间会一直亮着,直到玩家离开再回来;Kiradex 已经跟随《星露谷物语》,并且在玩家身处房间时也会重新打光。 | 第 1、第 5 和第 7 节11619 |
| 黑暗 | 黑暗是以格为单位的半径。《绿宝石》:不用闪光 1.5 格,用了闪光 4.5 格。 | 第 4 节5 |
| 投影 | 投影是单一色调,短于一个图块,落在一两个面上,无论高度如何都一样长。《绿宝石》的阴影精灵、Schlitter 的规则、Kiradex 的墙。 | 第 4、第 6 和第 7 节86374 |
| 加法光 | 加法光永远不会变暗;阴影需要 alpha 或美术本身。RealityKit 程序的混合模式是 add 和 alpha;unlit 材质按透明度混合。 |
第 7 和第 9 节7866 |
9. Apple 的方式:混合模式、会跳变的时钟与 Reduce Motion
Kiradex World 底下的引擎,就是本系列第一篇所阐述的那一套:把 RealityKit 场景当作 2D 渲染器,正交相机设在每个纹素对应整数个设备像素的位置,所有精灵材质都是 unlit。32 光、天气和时间不需要新的框架。它们需要加法混合和 alpha 混合,应用两者都已在用(前者是光池的 BlendMode.add,后者是 UnlitMaterial 的透明混合),还需要两个它尚未观察的通知,以及一个它只在一个文件里读取的环境值。下面的可用性是 2026 年 10 月 5 日各文档页面列出的内容,由 parse_apple.py 从保存的页面读取。26
两种混合模式,以及各自能画什么
MaterialParameterTypes.BlendMode 的文档描述是“Modes that describe how materials should be blended with content behind them”(描述材质应如何与其后方内容混合的模式),其页面恰好列出两个枚举值:add 和 alpha。784 关于 add,Apple 写道:“Blend by adding the material’s color to the background color, ignoring alpha”(忽略 alpha,把材质颜色加到背景颜色上进行混合),“also known as additive blending or linear dodge.”(也称为加法混合或线性减淡。)79 两者都标注为 iOS 18.0 及更高版本、iPadOS 18.0、Mac Catalyst 18.0、macOS 15.0、tvOS 26.0 和 visionOS 2.0。7879
混合模式属于材质的程序,而程序是一个 CustomMaterial,“A material that works with custom Metal shader functions”(配合自定义 Metal 着色器函数工作的材质),从 iOS 15.0 和 macOS 12.0 起可用。80 小镇里的其他一切都是 UnlitMaterial,“A material that doesn’t respond to lights in the scene”(不响应场景中光源的材质),从 iOS 13.0 和 macOS 10.15 起可用。71 Kiradex 恰好就是这样使用两者的:光池是一个 CustomMaterial,其程序的描述符设置了 blendMode = .add(还附有一句值得保留的注释:“The program says how to blend; the material says that it blends at all.”(程序说明如何混合;材质说明是否混合。)),云片和雾是 UnlitMaterial,以 .transparent 方式混合,不透明度分别为 0.55 和 0.7。229
阴影的结论由这个列表推出。加法光只能变亮:用 Glow.metal 的话说,“black adds nothing”(黑色什么也不加)。66 既然程序的混合模式只列出 add 和 alpha,unlit 材质又按透明度混合,那么在这个引擎里阴影只能用两种方式之一来画。一是在后面的内容上以 alpha 混合一层暗色,云片已经在这样做;二是烘焙进美术里,墙的阴影就是这样。782274 没有可以用阴影颜色来压暗的乘法混合。所以,移动的阴影是一张 alpha 片,固定的阴影属于图集。
两者在手机上的开销都没有测量。建筑篇指出它那两套新网格在 GPU 上的开销没有做性能分析,之后加入的 24 个加法光池、云片和雾也都没有做过。2312 简报增加了雨的精灵、水花、雪,以及云的第二档 alpha 强度;它们的开销同样没有测量:第 4 项对雨这样说明,第 5 和第 6 项中的雪、阴天和云的新速度也都没有做性能分析。
时钟,以及它何时跳变
舞台用一个休眠的任务每 60 秒重读一次时钟。19 SwiftUI 为随时间变化的视图提供了有文档的替代方案:TimelineView,“A view that updates according to a schedule that you provide”(按你提供的时间表更新的视图),配合时间表 everyMinute,其文档为“A schedule for updating a timeline view at the start of every minute”(在每分钟开始时更新时间线视图的时间表);两者都从 iOS 15.0 起可用。8182 按我对舞台任务的理解,它只有在下次醒来时,也就是最多一分钟后,才会知道时钟跳变了(越过午夜、进入夏令时、进入另一个时区)。时间跳变时,系统会告诉你。
UIApplication.significantTimeChangeNotification 按 Apple 的说法,会在“when, for example, there’s a change to a new day (midnight), a carrier time update, or a change to, or from, daylight savings time.”(例如进入新的一天(午夜)、运营商更新时间,或进入、退出夏令时)发布。其页面列出 iOS、iPadOS、Mac Catalyst、tvOS 和 visionOS,没有标版本。8326 NSSystemTimeZoneDidChange 是“A notification posted when the time zone changes”(时区变化时发布的通知),从 iOS 2.0 起可用。84 EnvironmentValues.timeZone 是“The current time zone that views should use when handling dates”(视图处理日期时应使用的当前时区),从 iOS 13.0 起可用,是 SwiftUI 在视图树中传递时区的方式。85
本系列的鲜活小镇篇为每个房间的时钟定了一份契约:每个房间都按服务器的 UTC 时间运行,按房间自己的时区读取,服务器在 welcome 和安静广场的响应中把两者作为 zone 和 clock 发送,这样无论手机的时钟怎么说,同一房间里的每个人看到的都是同一个时段。43 广场打开时,光照必须跟随这个时钟,而不是手机的时钟;而上面那些通知覆盖的是手机的日历,不是房间的:Apple 的页面把设备上的新的一天列为第一个通知的发布时机之一,对另一个时区的午夜只字未提。简报第 1 项说明两者如何配合。10 月 5 日时,应用里既没有房间时钟,也没有任何时间跳变的观察者;第 10 节开头说明了之后的变化。983
Reduce Motion
accessibilityReduceMotion 是表示“Whether the system preference for Reduce Motion is enabled”(系统的“减弱动态效果”偏好是否开启)的环境值,从 iOS 13.0 起可用。其页面的讨论很直接:“If this property’s value is true, UI should avoid large animations, especially those that simulate the third dimension.”(如果这个属性的值为 true,UI 应避免大幅动画,尤其是模拟三维的动画。)86 Kiradex 在 FigurePreview.swift 里读取它,世界代码的其他地方都不读。9 完全由运动构成的空气,如今并不读取它。
真实天气,以及闪光
Apple 提供了一个用于玩家真实天气的框架。WeatherKit 的文档描述是“Deliver weather conditions and alerts to your users”(向你的用户提供天气状况和预警),从 iOS 16.0 起可用。87 第 10 节拒绝了它,并在那里说明原因。
闪光有一个公开的阈值。W3C 关于 WCAG 2.2 成功准则 2.3.1“Three Flashes or Below Threshold”(三次闪光或低于阈值)这一 A 级准则的 Understanding 页面指出,网页“do not contain anything that flashes more than three times in any one second period, or the flash is below the general flash and red flash thresholds”(不包含任何在一秒内闪烁超过三次的东西,或者闪光低于一般闪光和红色闪光阈值),并给出原因:“Flashing content can cause migraines, dizziness, nausea, and seizures.”(闪烁的内容可能引起偏头痛、眩晕、恶心和癫痫发作。)46 Kiradex 是应用而不是网页,我把这条准则当作最接近的公开标准来用,并不主张它在法律上适用。
10. 简报:Kiradex 要做什么,以及必须通过的检查
作者综合。 以下是我的提议,由第 1 到第 9 节推导而来,写成相对于 2026 年 10 月 5 日 16:51 到 18:19 之间、在提交 cf3beb1e 到 66c116b0 时读到的应用的差异,当时工作树正在被编辑,每条注释都说明了它所依据的那次读取,读取的文件包括:Daylight.swift、Motion.swift、Air.swift、Glow.metal、PlazaStage.swift、PlazaRig.swift、PlazaClient.swift、HallView.swift、CollectorComposer.swift,锻炉的 air.py、town.py、buildings.py 和 interiors.py,10 月 3 日的 town.json,以及人物图表和它们的 characters.json。1297388 它不从任何宝可梦游戏中取用名字,也不画任何东西:不用它们的调色板、精灵、天气名称或声音。凡是借用机制的地方,第 8 节的规则都说明了出处,而外观是锻炉的。致敬篇的决定依然有效:Kiradex 保留自己的四个时段和自己的钟点。10 其中没有任何东西已经建成,也没有任何数字是在 iPhone 上测的:每个 Kiradex 数字都是在 Mac 上从源码读出或根据导出的图片计算的,设备上的开销一律未测量,检查的写法是为了让一次构建能够测量它们。检查中点名的每个类型、方法、文件和启动参数,要么如今已经存在,在这里按符号引用、在注释里按行号引用,要么在某一项中作为提议新增列出。检查 2 到 8、10 和 11 是我对调研资料中检查的修订:按最初的写法,有些要求 UI 测试做只有应用自身进程才能做的事,在应用运行时更改启动参数,从不携带某个值的地方读取该值,或者依赖应用并不存在的列表。UI 测试在自己的进程中运行,通过应用的无障碍值、标签和截图来观察它;-plazaHour 从启动参数读取,所以在一次启动中是固定的;舞台交给测试的唯一值,即它的无障碍值,携带的是玩家的最后一步,或在 -airProbe 下携带空气的报告,UI 测试读取的就是它,用舞台自己的注释说,是“since nothing else the simulator logs reaches them”(因为模拟器记录的其他东西都到不了它们那里)。192168 每条修订过的检查都说明了它需要什么。
应用自 10 月 5 日以来已经变了。 2026 年 10 月 15 日 23:53 重新读取,取自代码库 main 指向的提交 fc9d4876(当天 17:45 提交),每个文件都在不运行 git 的情况下从代码库自己的对象中取出,工作树中的每个副本都是相同的字节:服务器现在会在每个 welcome 中发送 zone 和 clock,整个世界一个时区,未配置时为 UTC,而 PlazaClient 会保存它们,按第 1 项的提议把服务器的毫秒数与手机的运行时间配对,以 roomDate 给出房间的时间。89 光照还没有读取它:PlazaStage 仍然保存一个每分钟从手机重读的 clock,并从中取得这一天;安静广场仍然不加入任何房间;在工作树的 Swift 中搜索,也找不到任何时间跳变的观察者。拒绝列表现在已经存在,即 scripts/forge/protected_names.txt,server/tests/test_names.py 用它来对照世界的区域名称、预设台词和镇民的昵称。89 下文的一切都是针对 10 月 5 日的读取写的,其中的“如今”指那一天;第 1 项和检查 2 如今应从客户端的房间时钟出发,第 10 项和检查 11 则应从那份列表出发。
1. 一天的时钟:保留钟点,渐变光照,读取房间的时钟
- 钟点不变。 6 点到 9 点是早晨,9 点到 17 点是白天,17 点到 20 点是傍晚,其余是夜晚(第 8 节,一天的分段)。1021
- 一种按时钟计算的渐变。 新的
Daylight.light(at:calendar:)接收一个日期和一个默认值为Calendar.current的日历,就像Daylight.now(_:calendar:)已经做的那样,返回在每个边界之后的前 30 分钟内,从上一个时段的色调线性插值到新色调、再乘以季节色偏的结果。舞台已经每分钟重新打光,所以渐变有 30 步。以图集平均亮度计,傍晚的变化从一次 0.077 变成 30 步、每步 0.0026,夜晚的变化从一次 0.108 变成 30 步、每步 0.0036。11 早晨的变化是如今最大的硬切,0.168,会变成 30 步、每步最多 0.0056;白天的 0.017 则变成每步最多 0.0006。4 - 渐变既不需要动画,也不需要新的计时器。 一天的光是时间的纯函数,所以读同一个时钟的两部手机显示同样的光,17:12 醒来的手机直接显示 17:12 的光,不必看过整个渐变。《水晶》式的硬切只在强制的
-plazaHour下保留,测试用它来直接查看某个时段;在这种情况下,relight()继续调用如今的Daylight.light(phase:season:)(第 8 节,变化)。2168 天气的一秒渐变(第 4 项)是唯一带动画的变化,它搭乘绑定层现有的逐帧更新,而不是自己的计时器。 - 房间的时钟。 下一项提议的小类型
LightClock是光照读取的唯一时钟;在广场里,它按鲜活小镇篇契约所规定的房间时间运行:服务器的 UTC 时间,按房间自己的时区读取,两者都在welcome和安静广场的响应中作为zone和clock发送,后者是那篇为多人功能受限、不打开 WebSocket 的玩家提供的唯一请求。两者中先到的那个把它们交给LightClock:welcome时由PlazaClient交,收到答复时由安静广场的请求交;时钟把服务器的时间戳与它到达那一刻手机的单调运行时间配对保存,于是它的now就是服务器时间加上此后的运行时间,以房间的日历表示;这样,一部墙钟错了好几个小时的手机,显示的光也与房间里其他每部手机相同,而Daylight.light(at:calendar:)和Weather.today(zone:date:)(第 3 项)使用的是这个时钟的日历和时区。只有在广场第一个welcome或安静响应到达之前,以及在广场之外,也就是房间或大厅里,才读取手机自己的墙钟和时区,在那些地方,鲜活小镇篇让收藏者保持自己的时钟(第 8 项说明这在门口意味着什么);LightClock会一直保留最近一次响应的时区和时间戳,直到下一次响应到来,它的状态会说明它在按哪个运行:由welcome给出的房间时钟、由安静响应给出的房间时钟,或手机的时钟。10 月 5 日时,服务器的welcome不带时区和时钟,PlazaClient的处理函数也什么都不读(本节开头说明了之后的变化);安静广场的响应是那篇的提议。在有响应带来它们之前,LightClock按手机的时钟运行,并在状态中如实说明。432176 - 在边界上变化,而不是晚一分钟。 位于新文件
Kiradex/World/LightClock.swift中的LightClock接收一个时间源(单调运行时间和墙钟,两者都可替换)、一个NotificationCenter,以及舞台的重新打光。它根据自己的日历计算光照必须变化的下一个时刻,即下一个时段边界、第 1 项中该边界 30 分钟渐变的结束点,以及第 3 项中它所在时区的下一个午夜,并为每一个都显式安排唤醒,此外还有渐变所需的每分钟一次的唤醒;唤醒时用它读到的时间调用重新打光。它还在自己的中心上观察UIApplication.significantTimeChangeNotification和NSSystemTimeZoneDidChange,收到任一通知都会重新打光,应对的是 Apple 页面所列的情况:手机自己时钟上的新的一天、运营商更新时间、进入或退出夏令时,以及手机时区的变化。这些覆盖的是手机的日历,而页面并没有说第一个通知会在另一个时区的午夜发布,所以房间的边界从不交给它。PlazaStage持有一个用系统时间源和NotificationCenter.default构建的时钟,取代它的clock状态和每分钟重读一次的休眠任务,这样房间时区的午夜、时段边界、夏令时变化和时区变化都不必等待最多 60 秒。时钟、中心和时间源都是参数,测试可以驱动它们;这个设计是我的。838419 - 强制的钟点仍是启动参数。
Daylight.forcedHour从进程的启动参数中读取-plazaHour,所以它在整次运行中保持不变,应用运行时无法更改;下面没有任何检查要求更改它。21 - 开销。 和如今一样,每分钟为地面、周边、物件和每个行走者各分配一次材质,这没有做过性能分析;渐变除了计算两种颜色的混合外不增加任何工作。20
提议与如今对比:每分钟光照下图集的平均亮度,由 figures/make_figures.py 根据已发布的 town.png 和 Daylight.swift 的色调计算。在 Mac 上读取,不是在手机上截取。
验收检查 1:渐变。 对 Daylight.light(at:calendar:) 做单元测试,给定一个固定时区的日历,在 16:59、17:00、17:15、17:29、17:30 以及 20:00 到 20:30 的每一分钟取样:在取样时刻上亮度单调变化,任意两个相隔一分钟的样本之间(16:59 与 17:00、17:29 与 17:30,以及 20:00 到 20:30 的每一分钟)图集平均值的跳变都不超过 0.004(脚本的值是 0.0036;测试从应用包中取 town.png 不透明像素的平均值),并且在 :30 及之后等于如今的色调。1211 同样的测试在 06:00 到 06:30 使用 0.006 的界限,因为早晨的渐变每分钟最多前进 0.0056。4
验收检查 2:房间的时钟与时间跳变。 在进程内进行,放在新的 KiradexTests/LightClockTests.swift 中,因为 UI 测试无法在应用内部发布通知,无法设置手机的时钟,而 -plazaHour 在应用运行时不能更改。每个测试都用一个自己的 NotificationCenter、一个由它控制的时间源(一个手动推进的运行时间和墙钟)以及一个记录调用的重新打光来构建 LightClock,不使用真实的计时器:测试自己推进运行时间,并要求时钟触发到那一刻为止已安排的一切。(a) 时间偏差的手机:给时钟一个 welcome,其 clock 在房间时区为 20:35:00,用时间源的运行时间打上时间戳,同时把墙钟设错三个小时;重新打光以房间日历中的 20:35 被调用,Daylight.now(_:calendar:) 把它读作 night,Daylight.light(at:calendar:) 给出夜晚的光,因为第 1 项的渐变已在 20:30 结束(渐变中的某个时刻,比如 20:05,会给出从傍晚之光向夜晚之光走了六分之一的光,正确的构建也无法让它与夜晚之光相符),而再次拨动墙钟不会改变任何东西。(b) 两个时区:手机的日历设在比房间早 10 小时的时区,两者都是固定偏移,这样不会有夏令时变化移动它们。在房间时区为 20:35 的那个服务器时刻,手机上是次日 06:35,Daylight.Phase(hour:) 把它读作早晨,已过第 1 项的渐变,而房间的 20:35 是夜晚,已过傍晚的渐变。重新打光得到的是房间的光,即夜晚色调乘以季节色偏(夏季亮度 0.593),而不是手机的光,即早晨色调乘以色偏(夏季 0.961);并且 Weather.today(zone:date:) 以房间的时区和房间的日期,也就是比手机早一天的日期来查询。读取手机时钟的构建会在光照和日期上都失败。(c) 边界,完全不发布通知:从房间时钟的 19:59:30 起,测试把运行时间推进 60 秒,断言一次已安排的唤醒在房间日历的 20:00:00 触发,而不是在这一分钟结束时;从 23:59:30 起推进 60 秒,断言在房间时区的次日 00:00:00 有一次唤醒,此时的 Weather.today(zone:date:) 是新一天的掷值。(d) 观察者,只测接线:不给房间时钟时,在测试的中心上发布 significantTimeChangeNotification 会以墙钟时间重新打光,NSSystemTimeZoneDidChange 也一样;测试断言的是这个调用,而不是系统会发布它,后者没有任何测试能做到。(e) 没有发布、没有到期的唤醒、也没有 welcome 时,不调用重新打光。(f) 安静广场,在进程内:给时钟的是安静广场的响应而不是 welcome,同样是 20:35:00,墙钟错三个小时,重新打光得到房间日历中的 20:35 和夜晚的光,与 (a) 相同,其状态说明来自安静响应。在应用中,一个 UI 测试在启动时像鲜活小镇篇的第一项安全检查那样把多人限制桩为开启,加上 -airProbe,并配合一个本地测试服务器,它对安静广场的请求回应一个固定的 zone 和一个与模拟器相差三小时的 clock,通过应用现有的 -plazaServer 参数访问(PlazaClient.endpoint,其注释为本地服务器给出 -plazaServer ws://127.0.0.1:8300/v1/plaza):安静请求不打开 WebSocket,所以它采用该 URL 的 HTTP 形式,把 ws 换成 http、wss 换成 https,并使用安静路由自己的路径,这个映射是我的提议;舞台的值在探针报告中加入 LightClock 的状态,指明来自安静响应,测试像那篇的检查一样断言没有打开 WebSocket。这需要服务器夹具和探针那一行,两者都是我的提议。文件:LightClock.swift(新建);持有 LightClock 的 PlazaStage.swift;PlazaClient.swift,它的 welcome 处理函数会把 zone 和 clock 交给它,而服务器在 10 月 5 日并不发送这两项;安静广场的请求,它会用自己的答复做同样的事;以及新的测试文件。7643 测试看不到的是 PlazaStage 传入的是 NotificationCenter.default 和真实的时间源;这只需在源码里读两行。按最初的写法,检查让 UI 测试在更改 -plazaHour 之后发布通知,这是任何 UI 测试都做不到的。建立在手机墙钟上、用一个手动发布的午夜通知代替房间午夜的时钟也不行:鲜活小镇篇的契约排除了前者,Apple 的页面没有承诺后者,所以检查依赖的是服务器的时间戳、显式安排的边界和两个时区的情形。1221194383
2. 人物保持本色
- 提亮。 行走者使用向白色提亮一半的光,即逐通道
(1 + tint) / 2。夜里为 (0.75, 0.79, 0.95),亮度 0.796,而场所为 0.593,亮 1.34 倍;傍晚为 0.913,早晨为 0.980。114 - 放在哪里。 新文件
Kiradex/World/PlaceLight.swift中的新值类型PlaceLight保存一天的光(来自第 1 项)和天气系数(第 4 项),并给出两种颜色:place,即一天的光乘以系数;walkers,即把一天的光向白色提亮一半,不受天气系数影响。PlazaRig用它取代自己唯一的light。场所材质使用place:build(tiles:collector:arrival:)最初创建的地面、周边和物件,light(_:lampsOn:)中和水波逐帧更新中的那些,以及showDoor(at:sheet:column:)里门的绘制。行走者使用walkers:add(other:figure:tile:facing:)中到达者的材质,以及animate(_:)中每一帧的材质。20 它是纯值,所以单元测试不用 RealityKit 就能驱动它;这个类型是我的提议。 - 为什么是一半。 第 8 节的规则说的是不动。提议一半,是因为 Kiradex 的行走者站在一个有加法光池和暗角的混合场景里,而不是在硬件调色板上,完全不加色调的人物,按我的理解,会看起来像贴在夜色上。这是要在设备上做出的判断,下面的检查为此留出了余地。1219
- 手里的卡牌。 它已经有自己的光,一个只为卡牌设置的
DirectionalLight。不变。20 - 探针上的收藏者帧、人物和画面。 在
-airProbe下,舞台的值还携带收藏者在视图点坐标中的帧,由PlazaRig.viewPoint(of:in:displayScale:)作用于玩家精灵的四个角得到,以及收藏者的人物遮罩:在其 32×40 的格子中,站立的收藏者所显示的每一幅画面里都不透明的像素,这些画面包括呼吸待机、眨眼以及PlazaRig.animate(_:)从中挑选的待机表情(抽动、张望、耸肩、思考和打盹),取自人物合成后的索引平面CollectorComposer.indexPlane(for:),其中像素既不是透明也不是橡皮擦时才算不透明,并按每一行写成游程。两者都加进舞台的.task如今在空气准备好后写入那里的报告中。对清单中默认的、面朝下的人物来说,遮罩是格子 1,280 个像素中的 290 个。20198830 这个值还携带收藏者材质显示的那一帧,即行走者的shown(朝向所在行乘以图表的 43 列,再加上画面所在的列,所以 0 就是面朝下的idle_a),每当它变化时重写,以及完全在视图内的发光格在视图中的帧,按它们在town.json中的格子标识。一个调试参数-plazaHoldStill(我起的名字)像-plazaHour一样从启动参数读取并在启动时固定,让站立的收藏者停在idle_a:没有呼吸,没有眨眼,也没有待机表情,否则animate(_:)会每隔 6 到 14 秒随机启动一个,玩家的第一个表情在 7 秒,第一次眨眼在 2.5 秒。20 在同一个参数下,Air.set(phase:season:)不绘制那些在行走者上方移动的图层,即云、雾(仅早晨,2.41)、落叶、萤火虫和烟囱的烟,它们都画在 2.4 到 2.45 的深度,高于行走者的 1 到 1.5;路灯光池保留,因为它们就是夜晚的光。探针的值还会说明是否有任何光池的平面与收藏者的帧重叠,并且在-airProbe下,舞台会绘制检查 3 的管线色板并报告它的帧。2230 这些全都是为检查 3 提议的新增内容。
Kiradex 的色调取自 Daylight.swift,以及提议的行走者光;色板由 figures/make_figures.py 绘制。这些是应用自己的值,在 Mac 上从源码读出;亮度在那里计算,不是在 iPhone 上测的。
验收检查 3:人物。 在 iPhone 18 Pro Max 模拟器上截取两张图,各自单独启动,一张用 -plazaHour 12,一张用 -plazaHour 22,两者都加 -plazaWeather clear(第 3 项),使当天的掷值不能改变光照,加 -plazaHoldStill(第 2 项),使两张图显示同一幅画面且上面没有任何东西,再加 -airProbe,由 UI 测试的 save 保存。舞台的值携带收藏者的帧、人物遮罩和画面,以及发光格的帧(第 2 项提议的新增内容),所以测试知道哪些像素是收藏者、哪些是亮着的窗户。它在每次截图前后各读取一次画面,要求四次都是第 0 帧,即面朝下的 idle_a;四次读取中只要有一次不是第 0 帧,保持就失效,检查失败。它还要求探针在夜间截图中报告收藏者的帧上没有光池。在评判任何截图之前,所有夹具都要冻结:四组启动参数,色板的图片、色调和位置,截图所针对的小镇导出数据(10 月 3 日的 town.json,按其哈希,连同它的图集),裁剪区域(来自探针的收藏者遮罩,其每个像素在帧中对应方格的中心取样;地面是视图去掉收藏者的帧和状态栏;发光像素见下文),管线,以及下面的容差,都是测试中的常量,改动其中任何一项都是对检查的改动,要公开进行,绝不能是从截图中得到的读数。12206819
亮度取自截图的存储颜色值。RealityKit 如何把色调和图片变成这些值,为本文保存的 Apple 页面没有说明:UnlitMaterial 页面列出了 init(applyPostProcessToneMap:),“Creates an UnlitMaterial with the given tone mapping setting”(用给定的色调映射设置创建一个 UnlitMaterial),但没有说 SpriteSheet.material(light:) 构建的普通 UnlitMaterial() 做什么,也没有说色调和以 .color 语义创建的纹理各自如何转换。7172 所以,区间要为每种管线分别设定,并且全部在任何截图之前,依据覆盖计算的第 E 部分:30
- 校准。 亮着的窗户和壁灯不加色调绘制,所以夜间截图自带一个固定的参照:在下面每种管线中,不加色调的颜色都按存储值输出。24 个发光格中,有 7 个在检查 8 的纸面重放中完整位于到达视图内,共 294 个发光像素,全都在任何光池的平面之外,并且距视图中心 450 点以内,处于暗角 900 点半径中清晰的那一半里。每个都在其方格中不被任何扫描线行覆盖的点上取样,这些像素上与图集存储值逐通道最大差的中位数必须不超过 0.02,即 255 级中的 5 级。如果不满足,说明路径中有色调映射或颜色转换,下面的管线都不适用:检查在读取区间之前就在那里失败,窗户所显示内容的区间要像这些一样从图集计算出来,写进测试,然后才能再次评判一对截图。201930
- 色调按存储值相乘,或色调和图片都在线性空间。 地面的夜昼之比按存储值计算为 0.588,把图片和色调从 sRGB 解码、相乘再编码回去则为 0.562;逐通道,红、绿、蓝分别为 (0.500, 0.580, 0.900) 和 (0.469, 0.554, 0.892)。区间为 0.46 到 0.69。以
idle_a保持的收藏者遮罩,在提亮下计算为 0.788 和 0.764,在完整夜晚色调下为 0.575 和 0.531,必须达到 0.67,即两个正确值中较低者与两个错误值中较高者的中点。 - 色调的数字被当作线性值。 如果图片被解码而色调没有,地面计算为 0.774,逐通道为 (0.719, 0.772, 0.951),区间为 0.67 到 0.87。遮罩在提亮下计算为 0.889,在完整色调下为 0.756,必须达到两者的中点 0.82。
-
是哪一种。 由色板而不是地面来指明管线。在
-airProbe下,舞台绘制一张由两种灰色组成的固定图片,存储值为 255 中的 51 和 102,施加每个通道都为 0.25 的固定色调,直接交给SpriteSheet.material(light:),从不经过PlaceLight或一天的光,画在光池上方的一个四边形上,位于视图中的固定位置:两块 16 点的方块并排,位于 Pro Max 440×956 点中的 x 24 到 56、y 462 到 478,即左边缘的中间高度,它最远的角距中心 197 点,因此在暗角清晰的那一半内。探针报告它的帧,地面的裁剪区域排除它,它的位置与其他夹具一起冻结。测试根据探针的帧断言它既不与收藏者的帧重叠,也不与七扇参照窗户中的任何一扇重叠;在到达时计算结果为 0 处重叠,最近的窗户相距 267 点。它像窗户一样避开扫描线行取样,两个钟点下的存储值相同。按存储值相乘,结果为 13 和 26;图片和色调都在线性空间时为 6 和 20;色调的数字被当作线性值时为 22 和 51;在任一块上,任意两种管线的差距都不小于 6 级。每张截图的色板必须落在某一种管线两个值的 2 级以内,每张截图都必须是同一种管线,然后适用该管线的区间和阈值。由于指明管线的是色板而不是地面,碰巧符合另一种管线区间的地面不能借此通过;测试还会记录所指明的管线,以及指明它时的 SDK,之后某次运行如果指明了另一种管线,就会失败,直到公开修改这条记录。色板及其图片是我的提议。30 -
通过条件。 校准成立;地面的夜昼之比落在所指明管线的区间内;色板在每张截图中都指明同一种管线,即测试记录的那一种;遮罩的比值达到该管线的阈值,并且高于地面的比值,这正是第 2 项提亮的体现;地面的通道比值通过下面两项断言;两个对照都失败;四次画面读取都是第 0 帧;夜间截图报告收藏者上方没有光池。其他任何情况都算检查失败。
- 区间。 每个区间是该管线计算出的比值上下各 0.1 并取整,这是根据计算设定的目标,不是测量。计算出的比值只针对图集;截图中还有路灯光池,它们只能让夜晚变亮,0.1 就是为它们留出的余地,在第一张截图之前选定。扫描线和暗角在两个钟点下画得一样,按我的理解,它们对两张截图的缩放是相同的。1291922
- 为什么是同一幅画面。 阈值只在两张截图是同一幅画面时成立,也只在那时成立。按存储值相乘,提亮后最小的通道是 0.75,所以在它之下任何颜色保留的白天亮度都不会少于 0.75,同一幅画面下遮罩的平均值也不会。跨两幅画面就可能了,因为遮罩固定的是取样哪些像素,而不是这些像素持有的颜色:它的白天亮度从
doze_b的 0.2989 到think的 0.3161,一个正确的构建如果白天截到think、夜里截到doze_b,计算结果为 0.746,而在 11 幅画面的 121 个有序对上,一个给行走者加色调的构建可以达到 0.610。所以要保持静止。在线性空间里,sRGB 曲线笔直的底部会让最暗的颜色保留不到 0.75,这就是为什么每个阈值都是计算出的平均值,而不是那个下限。3011 - 空气会做什么。 空气绘制的一切都在每个行走者之上:云在深度 2.4,雾(仅早晨)在 2.41,落叶、萤火虫和烟在 2.45,光池在 2.5,而行走者在 1 到 1.5;除夜晚外每个时段都显示云。它最深的阴影把下面的东西乘以 0.763,白天落在遮罩上时,如果阴影压暗的是编码后的值,会把一个给行走者加色调的构建抬到按存储值 0.754、线性 0.696、色调数字被当作线性时 0.991;如果它在编码前于线性光中压暗,则为线性 0.608、色调数字被当作线性时 0.865。渲染器到底怎么做,保存的页面没有说明,而除了 0.608 那一种情况,错误的构建在其他每种情况下都会通过阈值。2230
- 为什么要保持。 保持让空气离开收藏者,而这项检查离不开这一点。用一个探针标志反复重拍直到收藏者不被遮挡也可以,但云横向每秒漂移 1.536 像素、上下每秒 0.576 像素(两条轴上分别朝哪个方向,代码并没有定下来:精灵表测得的约定,即偏移从纹理左上角算起,适用于它们自己手工构建的四边形,而云是一个生成的平面),所以一片穿过收藏者帧的阴影,会在漂移把它带过 32 像素、也就是帧的宽度所需的 21 秒(20.8 秒)内停留在帧的一部分上,阴影越宽停留越久,而图片大约一半被覆盖;保持则在每张截图里都解决了这个问题;我选择了保持。光池无法挪开,因为它们正是被测试的光,而且它们不动:在到达时,收藏者 32×40 像素的帧离最近的光池平面有 52 像素,所以探针的光池标志是对这一计算的保护,一旦被置位,检查就失败,而不是重拍。22930
- 为什么不用整个帧。 收藏者的整个帧不能当裁剪区域:它大部分是地面。默认人物在第一幅站立画面中覆盖 1,280 个像素中的 408 个(只算身体图表是 377 个,导出中所有人物图表叠在一起是 581 个),其余按图集平均颜色、按存储值相乘,一个正确构建的整个帧计算为 0.638,低于遮罩必须达到的 0.67,只比地面高 0.049,而一个给行走者完整加色调的构建会得到 0.584。30
- 另外两项断言。 它们防止一对截图仅仅因为彼此相似而通过:地面的夜昼之比逐通道计算时,要像夜晚色调 (0.5, 0.58, 0.9) 那样排序,蓝高于绿、绿高于红,并且每个都在所指明管线计算出的对应通道的 0.15 以内(对第一种管线,两组中的任一组均可)。
- 对照。 第三次启动用
-plazaHour 22加一个提议的调试参数-plazaNoTint,它像-plazaHour一样在启动时固定,在它之下PlaceLight.place在每个钟点都返回一天的光;这次必须不落在区间内,它的地面比值接近 1,这表明区间会拒绝未加色调的输出。一个忽略PlaceLight的渲染器在两个钟点给出同样的画面;对这项检查来说,它约为 1 的地面比值在任一区间都不合格,平坦的通道比值也不满足排序。第二个对照是第四次启动,用-plazaHour 22加另一个提议的调试参数-plazaLiftAgain(我起的名字),在它之下SpriteSheet.material(light:)会把收到的每个色调再向白色提亮一半,这次也必须失败。这是一个PlaceLight给出正确值、而材质却不正确的构建:按存储值相乘,它的地面计算为白天的 0.794,各通道为 (0.750, 0.790, 0.950),遮罩为 0.894,白色窗户不变,未加色调的对照仍读作 1。单看地面,0.794 加上 0.750 的红色比值,会落在色调数字被当作线性时的区间 0.67 到 0.87 之内,并通过所有针对地面的断言,所以要由色板来裁决。色板让它失败:它的色调达到 0.625,读数为 32 和 64,距最近一种管线的值 13 级。30 - 区间不移动。 如果第一对真实截图落在其管线的区间之外,检查就失败,原因要到渲染器或计算里去找;不能为了迁就它而移动区间。如果改为从第一对截图取区间,就会让一个禁用了色调的渲染器通过,这就是为什么夹具和区间都要事先固定,第一对截图只是在已经写好的区间之间做选择。
- 单元测试。
PlaceLight的单元测试补上精确的部分:在夜晚色调、夏季、晴天时,walkers为 (0.75, 0.79, 0.95),place就是色调本身。11 - 最初的写法。 检查原本只有一张夜间截图,要求收藏者的平均亮度至少是白天截图的 0.75,同时地面在 0.55 到 0.65 之间,没有指定裁剪区域、夹具、画面、管线或对照;以上所有内容都是我的修订。12
3. 天气:五种状态,每天掷一次
- 状态。
clear、overcast、rain、heavy rain,以及只在冬季出现的snow。早晨的雾、秋天的落叶和夜里的萤火虫保持原样,叠加在上面。天气只在室外:Air找不到天空的地方(它的outdoors),也就是房间和大厅,不会有雨滴,也不受天气系数影响;这是我的选择。1222 - 每天掷一次,同一区域的所有人相同。
Weather.today(zone:date:)对区域标识符和该区域内的日期"zone|yyyy-mm-dd"取 SHA-256,把摘要的前四个字节读作一个无符号大端整数,再对 100 取模。这个掷值按累积方式与该季的表格匹配:按列出的顺序依次取各状态,把每个状态的权重加到累计总数上,第一个使累计总数超过掷值的状态就是当天的状态。在春季,掷值 0 到 54 为晴,55 到 79 为阴,80 到 99 为雨。11 表格是我定的:春季晴 55、阴 25、雨 20;夏季晴 65、阴 20、雨 10、大雨 5;秋季晴 45、阴 30、雨 20、大雨 5;冬季晴 40、阴 30、雪 30。12 它的形状遵循《星露谷物语》的季节概率(多雨的春天,雨会变大的干燥夏天,冬天不下雨);数字是 Kiradex 的(第 8 节,天气的排期)。18 - 一年的情况。 在一个区域对 2027 年运行,掷值给出:春季 92 天,晴 53 天(58%),阴 22 天(24%),雨 17 天(18%);夏季 92 天,晴 61 天(66%),雨 13 天(14%),阴 12 天(13%),大雨 6 天(7%);秋季 91 天,晴 40 天(44%),阴 30 天(33%),雨 17 天(19%),大雨 4 天(4%);冬季 90 天,晴 36 天(40%),阴 29 天(32%),雪 25 天(28%)。在这一年的 82 个湿天,即雨、大雨或雪的日子里,这里的潮湿期指连续的湿天,三者任意混合,所以一天雨之后接一天大雨是一个两天的潮湿期。共有 65 个潮湿期,其中 50 个一天、13 个两天、2 个三天,最长 3 天。两个区域在 1 月的 30 天中有 8 天相同,因为每个区域各自掷值,而同一个区域和日期总是给出同一种状态。114 春季的雨 18%,正好落在《星露谷物语》18.3% 的基础概率上。184
- 变化何时到达舞台。 掷值的日期按房间的时区读取,所以当天的状态在该时区的午夜变化。舞台在每次重新打光时读取它,而第 1 项的
LightClock会根据自己的日历在房间的午夜安排一次唤醒,所以新一天的状态会准时到达,而不是最多晚一分钟,也不依赖一个 Apple 页面只针对手机自己的日子做出承诺的系统通知。新出现的舞台在第一次重新打光时读取当天的状态,并且不加渐变地显示它。所以第 4 项的渐变,只有在房间午夜时开着广场的人才看得到,而那正落在 Kiradex 的夜晚里。832143 - 事先可知。 任务板用一行 Kiradex 自己的话写出明天的天气,就像《星露谷物语》的电视那样。17
- 文件。 新文件
Kiradex/World/Weather.swift存放各种状态、每种状态的光照系数和状态文字、掷值、任务板那一行(Weather.boardLine(zone:date:),给出同一区域第二天的状态)、云的速度(第 6 项)以及强制状态;Air.swift负责雨、水花、雪和阴天(第 4 项);锻炉的air.py负责rain.png、splash.png和snow.png,格子为 8×8 到 8×16;PlazaStage.swift的relight()读取状态,把它的系数交给绑定层、把状态交给Air,它的PlazaView.words把状态文字加到状态栏;以及QuestBoardView.swift,由广场视图(PlazaView)和“我的”标签页以表单形式呈现,负责任务板那一行。129019Daylight.swift不接收天气:它的光仍只是时间的函数,天气系数在PlaceLight中施加。调研资料把天气系数放在Daylight里,把任务板那一行放在PlazaStage.swift里;渐变不能存在于一个时间的函数中,任务板是独立的视图,所以这两处修正都是我的。 - 为测试强制天气。
-plazaWeather加一个状态名,可以在一次启动中强制当天的状态,它从启动参数读取,并像-plazaHour一样在启动时固定;下面的检查会用到它。这是我新增的。
提议:每日掷值一年的结果,取自 kiradex_proposals.txt 第 6 部分,在 Mac 上计算。
验收检查 4:掷值。 一个单元测试在区域 America/Los_Angeles 中计算 2027 年 365 天的 Weather.today,并与 measure_kiradex_proposals.py 第 6 部分打印的结果比对:每个季节各状态的天数,按上文定义的 65 个潮湿期中的 82 个湿天,最长 3 天,以及 1 月 30 天中 Europe/London 与之相同的 8 天。1211 它还断言,无论测试把设备时区设成什么,同一个区域和日期都给出同一种状态,因为日期是在所指定的区域中读取的。任务板那一行是我加的:同一个测试断言 Weather.boardLine(zone:date:) 给出的是 Weather.today 为同一区域第二天返回的状态,并断言五种状态的状态文字互不相同,且都不为空。按最初的写法,检查要把一年逐日与脚本的输出比对,而脚本打印的是计数,不是每天的结果,还让一个服务器测试去检查服务器并不做的掷值;调研资料中的湿天连续段(其第 4.3 节)统计的是同一种状态的连续段,所以雨之后接大雨算两段,得出的 67 这个数,脚本已经不再打印。
4. 雨
- 雨滴。 iPhone 18 Pro Max 的视图中 15 滴,该视图显示 165×358 个世界像素(这是角色篇的数字,这里没有重新测量):沿用《绿宝石》每 10,000 平方像素 2.60 滴的密度。大雨为 37 滴。1191
- 下落。 一滴雨是锻炉绘制的、Kiradex 自己形状的一道线,以约每秒 868 像素、偏离竖直 26.6 度的角度下落 0.301 秒(《绿宝石》的 18 帧),然后在地面上溅起 0.117 秒的水花(它的 7 帧)。11 位置取整像素(第 8 节,雨滴)。在每秒 120 帧时,每帧移动 7.2 像素,雨线的长度可以盖住这个间隔。114
- 雨滴放在哪里。
Air增加一个RainField,这个值保存针对视图尺寸和当天状态的雨滴位置,并按帧时间推进它们,取整到像素;Air.update(_:camera:view:)推进它,并为每滴雨放置一个实体,而Air.set(phase:season:weather:reduceMotion:)取代如今的set(phase:season:),用来开关雨滴、雪花和阴天片。雨滴属于Air,按它头部注释的说法,Air中没有任何东西受当天色调影响,所以夜里的雨滴按锻炉画的样子绘制;它在夜色中是否合适,要在设备上判断。Air还在它的报告中保存雨滴数量,在-airProbe下,舞台在准备空气的.task中,把报告复制到自己的值里,由一次改为每秒一次,直到玩家迈出第一步;从那以后,rig.onStep像如今一样把步伐轨迹写到那里,因为步行测试在那里读取它。读取报告的检查 5 和 10 不迈步。这些全都是提议。2219 - 光照。 场所的光乘以 13/16(第 8 节,雨的光):图集白天亮度从 0.449 变为 0.365,雨夜为 0.214。11 行走者保持第 2 项的提亮,不受天气系数影响。发光格和光池不变。
- 一秒渐变,以及由谁来执行。 进入和离开雨天时,场所的光在 1 秒内渐变(第 8 节,变化)。12
PlaceLight(第 2 项)保存天气系数、它正在渐变到的目标系数,以及变化以来的时间;当被告知一个带渐变的新系数时,它会在被推进的时间里用 1.0 秒从原来的系数线性移动到新系数。PlazaRig.update(_:)已经在运行世界的刻度(行走者、镜头和卡牌),然后每帧转动展示柜、推进水波并更新空气,由它来推进这个值;在place发生变化的那一帧,它像一次重新打光时light(_:lampsOn:)所做的那样分配三种场所材质,门显示期间还包括门的材质。舞台的relight()带渐变地传入每种状态的系数(晴 1,阴 0.9,雨 13/16,大雨 0.75,雪 1),只有舞台第一次重新打光时例外,那一次立即设置。除此之外没有任何东西给光加动画,也不增加计时器。2019 - 假设。 沿用的数字假设 60 赫兹;与掌机不同的帧率改变的是每帧的步长,而不是速度。12
- 开销。 15 到 37 个精灵实体及其水花,没有做性能分析。11 渐变期间,绑定层在一秒内每帧都分配三种场所材质,门显示期间还包括门的材质,60 赫兹时 180 次、120 赫兹时 360 次(有门时为 240 和 480 次),而一次重新打光每分钟是 3 次(门显示期间 4 次),有水的地方水波每秒 12 次;这是从代码数出来的,设备上的开销没有测量。420
验收检查 5:雨。 分两部分。在应用中:在 Pro Max 模拟器上以 -plazaHour 12、-plazaWeather rain 和 -airProbe 启动的 UI 测试,像 testTheChimneysSmoke 等待 smoke 18 那样,等待舞台值中的空气报告显示 15 ± 1 的雨滴数;这需要第 4 项提议的报告及其每秒一次的复制,而测试不迈步,所以复制会保持这个值。6819 在进程内:一个单元测试以每秒 120 帧把 Pro Max 视图的 RainField 推进 10 秒的模拟雨,断言每个雨滴位置都是整数、数量为 15;把处于一天之光的 PlaceLight 带渐变地告知下雨并推进,断言 place 在 1.0 秒标记前一帧仍高于一天之光的 13/16,并在其后一帧之内到达 13/16;并断言 walkers 在有无天气系数时相同。接线部分,即 PlazaRig.update 推进它的 PlaceLight、在 place 变化时分配材质,只需在源码中读几行。按最初的写法,检查要从绑定层日志中读取这四项,而那个日志只到达 Mac 的控制台,到不了测试。12
5. 阴天、雪,以及没有闪电的大雨
- 阴天。 云片的不透明度从 0.55 升到 0.8,光照带上 0.9 的灰。12 用已发布的
cloud.png,最深的阴影从 0.763 变为 0.655,图片的平均值从 0.950 变为 0.927,灰色下图集的白天亮度变为 0.404。4 灰色像雨一样通过PlaceLight渐变;云片的不透明度由Air.set(phase:season:weather:reduceMotion:)立即设置,因为状态只在午夜变化,而那时云片是隐藏的(Air.set在夜里隐藏云)。22 - 雪。 只在冬季,由锻炉绘制:缓慢的雪花,带着落叶那样的摇摆,由
Air像管理落叶一样管理,地面保持冬季的色偏;雪不受天气系数影响,这是我的选择。1222 - 大雨。 光照进一步压暗到 0.75:图集白天亮度 0.337,大雨之夜 0.198。124
RainField保存 37 滴。11 - 没有闪电闪光。 大雨不产生闪光。第 2 节实测的《绿宝石》雷暴在任何一秒内最多只有一道闪电,低于 W3C 每秒三次闪光的限制,所以经典作品能通过这条准则。446 拒绝它与是否通过无关。原因是闪烁内容“can cause migraines, dizziness, nausea, and seizures”(可能引起偏头痛、眩晕、恶心和癫痫发作),这是 W3C 的原话,而我的前提是孩子们可能长时间使用这座小镇;以这种方式被使用的世界不需要它。4612
验收检查 6:没有闪光。 在进程内:一个单元测试以每秒 120 帧把处于一天之光的 PlaceLight 推进 10 秒的模拟时间,在第 2 秒带渐变地告知大雨、在第 6 秒告知晴天,并断言 place 作为图集平均值上的亮度,相邻两帧之间的变化从不超过 0.02,并且它会到达一天之光的 0.75 再回来。进入大雨的渐变在一秒内把白天的 0.449 带到 0.337,60 赫兹时每帧 0.0019、120 赫兹时每帧 0.0009,所以这个界限绰绰有余;硬切则是 0.112 的一步。4 PlaceLight 不接收 Reduce Motion 的输入,所以按其构造,渐变在设置开启或关闭时都一样(第 9 项);设置改变的是什么,由检查 10 来断言。在 Mac 上对模拟器录屏可以交叉核对,但录屏不一定包含应用绘制的每一帧,雨滴本身的运动也会改变每帧的平均值,所以以单元测试为准。按最初的写法,检查要求截取每一帧,而这是 UI 测试做不到的。12
6. 会移动的云影
- 速度。 云以固定的每秒 16 像素漂移,而不是 1.64,即每秒一个图块:是如今步行速度每秒 60 像素的 0.27,应用的刻度时钟按运动篇简报的设定给出这个步行速度(每刻度 1 像素,每图块 16 刻度,每秒 60 刻度)。这个固定数字是提议,第 2 节的图表把它与那个步行速度并列;它接近《绿宝石》雾的比值 0.25,比它的云的 0.50 慢(第 8 节,空气)。以这个速度,云走过与 Pro Max 视图高度 358 像素相等的距离需要 22.4 秒;它穿过视图需要多久,取决于当天的方向(见下文):2027 年 1 月 1 日,方向为自东起 215.16 度,竖直分量每秒 9.21 像素,水平分量 13.08 像素,所以它穿过视图高度要 38.9 秒,穿过宽度要 12.6 秒。1147773 这是如今漂移速度的 9.75 倍。4
- 阴影。 保留云片最深的阴影 0.763,以及大约覆盖图片一半的面积。9
- 方向。 当天的方向由摘要的第五个字节固定,也就是掷值不读取的第一个字节,取该字节乘以 360/256 度,自东逆时针计,这样同一区域的每个人看到的云都朝同一方向走。在 2027 年 1 月的 30 天里,脚本得到 30 个不同的方向。11 调研资料说的是掷值的“second byte”(第二个字节),而那是掷值读取的四个字节之一;第五个字节是我的修正。12
- 文件。
Weather.swift中的Weather.cloudVelocity(zone:date:)以每秒像素给出当天的速度;Air.update把它除以 384 加到云的偏移上,取代如今固定的每秒SIMD2(0.004, 0.0015),并以每秒像素把漂移保存在它的报告中。两者都是提议。22
验收检查 7:云。 在进程内:一个单元测试断言 Weather.cloudVelocity(zone:date:) 的大小为每秒 16 像素,同一区域和日期相同,并且在给定日期之后的 29 天中至少有一天不同(脚本发现 2027 年 1 月 2 日到 30 日这 29 天全都与 1 月 1 日不同)。在屏幕上:在 -plazaHour 12、-plazaWeather clear 且收藏者静止不动的情况下,相隔 5 秒截两张图,用能最好对齐云影的平移量叠合,显示出 80 ± 3 个世界像素的漂移,这同时也测量了 RealityKit 是否如第 7 节所假设的那样在缩放之后应用偏移,以及 Air.update 是否如第 6 项所说加上了速度。按最初的写法,检查要读取绑定层日志,并让两台模拟器共用一个日期,而这是一次启动无法设置的。122211
7. 落在地面上的建筑阴影
- 处理步骤。 在
town.py的paint中、墙的阴影之后加入一个锻炉处理步骤。对每座建筑,从顶行到底行逐行处理,紧挨该行最东侧建筑格东边的那一格,接受与墙脚相同的处理:在离建筑最近的 4 个像素列上,在它自己的色阶上调暗一档。只有当这一格在小镇字母中是地面、草坪、小路或铺石时才加阴影;如果它上面有道具、树、售货亭、另一座建筑、栅栏或水,这一行就不加阴影,处理步骤也绝不越过它去更东边的格子。所以每条阴影带都贴着自己的墙。光从左上方来;阴影落在一个面上,短于一个图块,一层和两层的建筑宽度相同(第 8 节,投影)。126311 关于行和道具的规则是我的;调研资料按每座建筑的列来定义这个步骤,而它的脚本从建筑的包围盒取列,所以较窄的屋顶行或底行,阴影带会离墙三格。 - 落在哪里。 七座建筑共有 39 行。其中 33 行东边的格子是地面,28 格草坪、5 格小路,加上阴影;6 行里有道具(5 个挡路的,1 个花坛,在
house-mine的底行旁边),保留自己的美术。11 按每格 16 行、4 列计,共 2,112 个阴影像素。4 - 底部以下不加。 每列最低一个建筑格下方的格子会再增加 40 格,其中 35 格是地面(22 格小路、8 格草坪、5 格铺石);简报不给它们加阴影。114 建筑正面朝向视图中光的那一侧,按我对 Schlitter 的“one or two wall faces”(一两个墙面)的理解,在那里加一条带会让建筑看起来像悬浮着。1163
- 烘焙,而不是运行时绘制。 阴影进入地面图集。它不随钟点移动(这里测量的游戏没有一款会移动阴影),夜里也没有任何开销。12
- docstring 成真。
buildings.py那一行“The ground shadow stays off the building: it belongs to the ground layer.”(地面阴影不放在建筑上:它属于地面层。)届时描述的就是一个实际存在的东西。24
提议:建筑阴影落在哪里,取自 town.json 和 kiradex_proposals.txt 第 3 部分;这是平面图,不是渲染图。
验收检查 8:阴影。 一个锻炉检查统计出 33 个阴影格,并拒绝以下情况:西邻不是同一行同一座建筑的格子的阴影格,落在非草坪、小路或铺石格上的阴影,以及宽于 4 像素的阴影带。1211 在 Pro Max 模拟器上、到达时、任何点击之前,以 -plazaHour 12、-plazaWeather clear 和 -airProbe 启动截图,显示 house-dawn 全部五行东侧位于第 18 列小路上的阴影带,以及 house-moss 第 3、4 行的阴影带,而它第 5 行东侧有道具,没有阴影带。这一帧来自用绑定层自身几何在纸面上重放镜头:收藏者到达 (19, 13),镜头以世界像素停在 (312, 278),视图横向覆盖 229.5 到 394.5,所以这些阴影带都在画面内,并至少留有 25 像素的余量;在评判截图之前,探针的视图和世界尺寸要确认重放中的 165×358,探针的帧要确认在 -airProbe 下绘制的检查 3 管线色板不与这里点名的任何阴影带重叠,到达时它们离色板 144 点或更远。276830 改为沿着 testTheChimneysSmoke 的路线走到 house-mine 行不通:用同样的方法重放,那条路线的点击落在导出数据标为道具的格子上,所以收藏者不会移动,而且从到达画面出发,测试能做的任何点击都无法把 house-mine 的全部阴影带纳入 10.3 个图块宽的视图。邻接规则和到达画面是我对调研资料中检查的修订。
8. 室内
Kiradex 已经跟随《星露谷物语》:房间和大厅以与广场相同的代码、按同一个时钟连同灯一起重新打光,那个时钟如今是手机的(第 7 节)。保留这一点(第 8 节,室内),但要注意第 7 节点出的差异:《星露谷物语》会让玩家所在房间的光保持不变,直到玩家离开再回来,而 Kiradex 每分钟重新打光,这是 Kiradex 的选择。16 当广场改用房间时钟时(第 1 项),房间和大厅仍用手机的时钟:按鲜活小镇篇的契约,在广场之外收藏者的时钟仍是他们自己的,而大厅用它自己的注释说是一个“off the plaza”(在广场之外)的地方,构建时没有客户端。在手机时区与房间时区不同的地方,光可能会在门口改变,而代码已经把两次穿越都放在黑色帷幕之下:经由它进入房间和大厅,广场则“out of the door gone in by, the veil lifting”(从进去的那扇门出来,帷幕揭开)而回来。广场一直留在叠于其上的那个地方之下,它的 LightClock 也随之保留,所以在回来的路上,它仍然保存着最近一次 welcome 或安静响应的时区和时间戳。这条规则是我的。436919 然后加上《星露谷物语》的一项改进,即它的 WindowLight:房间的窗户显示室外的时段,于是夜里的房间之所以暗,是因为它的窗户暗了。125369 锻炉的 interiors.py 如今会画房间的窗户,一扇普通的和一扇带窗帘的(window 和 curtained_window,由房间网格中的字母 N 和 K 放置);它要为每块窗玻璃画出白天、傍晚和夜晚的变体,并把每扇窗户所在的格子随房间一起导出。绑定层像把书架和书桌叠在它们的格子上那样(addQuad),把对应时段的变体叠在每个窗户格上,舞台的 relight() 把时段传给它。这些全都是提议。9220
验收检查 9:房间。 一个锻炉检查在房间图集中找到每块窗玻璃的三个变体,并为房间网格中的每个 N 和 K 找到一个窗户格。在 Pro Max 模拟器上截两张图,各自单独启动,分别用 -plazaHour 22 和 -plazaHour 12,通过“我的”标签页的 me-room 按钮进入玩家自己的房间,这不需要服务器,并在一楼格 (3, 1) 的带窗帘窗户上分别显示夜晚和白天的窗玻璃。按同样的纸面重放,从房间的到达点 (7, 10) 出发,视图横向覆盖图块 2.34 到 12.66,纵向覆盖整个房间,所以那扇窗户在画面内,而 (12, 1) 的普通窗户被边缘截断。9227 这项检查是我的,加上它是为了让每一项都有一项检查。
9. Reduce Motion
PlazaStage 从环境中读取 accessibilityReduceMotion,与一个像 -plazaHour 一样在启动时固定的调试启动参数 -plazaReduceMotion 做逻辑或,然后在每次重新打光时以及环境值变化时,把结果传给 Air.set(phase:season:weather:reduceMotion:)。参数和形参都是提议。开启时,雨和雪的数量降为三分之一,下落速度减半(视图中雨 5 滴、大雨 12 滴,每秒 434 像素),落叶、萤火虫、雾和烟保持静止,云也停下来,速度取为零。光照不做硬切:色调保留 30 分钟的渐变,天气保留 1 秒的渐变,由 PlaceLight 在没有 Reduce Motion 输入的情况下执行。按我的理解,亮度的缓慢变化并不是这个设置所针对的运动,把它改成硬切,就是用一个突然的变化换掉一个缓慢的变化:进入雨天的渐变把图集白天平均值从 0.449 带到 0.365,60 赫兹时每帧 0.0014,而硬切是一步 0.084,超过检查 6 每帧 0.02 界限的四倍。12864 调研资料让天气在 Reduce Motion 下硬切而不是渐变,并且只点了雨、雪、落叶、萤火虫和云;保留渐变以使本项与检查 6 一致,以及让第 7 节发现无论设置如何都在动的雾和烟静止下来,是我的改动。《星露谷物语》的雪透明度设置,是让玩家把天气调弱的先例。17
验收检查 10:Reduce Motion。 SwiftUI 的这个环境值是只读的,声明为 var accessibilityReduceMotion: Bool { get },所以应用和测试都无法设置它;只有系统设置能改变它。86 因此第 9 项增加了 -plazaReduceMotion。以 -plazaHour 12、-plazaWeather rain、-plazaReduceMotion 和 -airProbe 启动的 UI 测试,等待携带雨滴数(第 4 项)和云的漂移(第 6 项)的空气报告显示雨滴数 5 ± 1、漂移为 0。检查 6 的单元测试表明渐变保持在界限之内,因为渐变没有能改变它的 Reduce Motion 输入。再手动做一次:在模拟器的“设置”中打开 Reduce Motion,不加该参数,同一份报告显示同样的数量。按最初的写法,检查要读取一份雨的日志,而测试中没有任何东西能打开这个设置。1219
10. 名字与美术
10 月 5 日时,应用没有拒绝列表。调研资料版本的这一项说天气图片要通过“the deny list”(拒绝列表),而提出需要它的道路篇说“The deny list does not exist yet”(拒绝列表尚不存在),并把建立它列为自己简报的一部分;10 月 5 日搜索 Kiradex 代码库,也没有找到这样的列表。1293 所以本项说明 10 月 5 日能做什么,以及什么要等那份列表;列表后来已经加上(见本节开头)。
- 字符串。 新的字符串是五种状态的状态文字和任务板那一行。它们要通过道路篇简报所要求的拒绝列表,那份列表在 10 月 5 日并不存在,如今代码库中已有
scripts/forge/protected_names.txt。它们还要对照唯一公开的那份列表,即致敬篇的“Never”列表,其第二项点名了任何功能都不得使用的系列词汇。10 - 图片。
rain.png、splash.png、snow.png和窗玻璃由锻炉绘制,并对照同一列表的第一项审查,该项拒绝任何“taken, traced or redrawn from any Pokémon game”(取自、描摹或重绘自任何宝可梦游戏)的图块、精灵或调色板。10 - 调色板。
Daylight或Weather中的任何颜色值,都不等于缩放到 0 到 1 的pokecrystal调色板颜色,纯白除外。白色是让美术保持原样的那个乘数,不是从任何人那里取来的颜色:如今的白天色调和夏季色偏都是白色(在Daylight.swift中),而白色也出现在《水晶》的背景和人物调色板里,所以按调研资料的写法,这条规则会让已发布的应用本身不合格。这个例外是我的修正。122821
验收检查 11:名字与美术。 一个新的单元测试 KiradexTests/NamesTests.swift 把致敬篇“Never”第 2 项中的词条作为列表放在测试目标里,断言任何状态的状态文字和任何任务板那一行都不包含其中任何一个,忽略大小写,并且像那一项拒绝“Poké-anything”(任何 Poké 开头的东西)那样,拒绝任何以“Poké”开头的词;同一个测试还读取代码库的拒绝列表,那份列表在 10 月 5 日并不存在,所以检查的这一半在那天无法运行,它现在是 scripts/forge/protected_names.txt:该文件作为测试资源打包进 KiradexTests,它的条目每行一个,跳过以 # 开头的行,按文件头所说(“matched without regard to case as a whole word or phrase”(不区分大小写,按整词或整个短语匹配))不区分大小写地按整词或短语匹配;打包是我的提议。89 一个脚本把 Daylight.swift 和 Weather.swift 中的每个颜色常量,与缩放到 0 到 1 的 bg_tiles.pal、npc_sprites.pal 和 roofs.pal 中的每种颜色比较,除纯白外找不到任何匹配;它在作者的证据文件夹里、反编译克隆旁边运行,所以没有任何反编译数据进入应用或锻炉,正如同一份“Never”列表的第四项所要求的。10 月 5 日对已发布的 Daylight.swift 运行,它的 8 个颜色常量对照这三个文件的 404 种颜色,找到两处匹配,都是纯白,即白天色调和夏季色偏,没有其他匹配;除此之外最接近的一对,在一个通道上相差整整一个 5 位级。对图片的审查连同每张图片的截图一起记录。121028 按最初的写法,检查要让图片通过一份应用在 10 月 5 日并没有的拒绝列表。
本简报不包括的内容
- 玩家的真实天气。 WeatherKit 会把广场变成一个天气应用,还需要玩家的位置。经典作品的天气属于世界,是排定的、共享的(第 8 节,天气的排期)。8712
- 随太阳移动的阴影、照在精灵上的点光源,以及景深。 用 HD-2D 自己制作人的话说,这一级“costs more than you’d think”(成本比你想象的高),而且正如世界篇所记录的,它的玩家会关掉模糊。5932 这里测量的游戏没有一款会随钟点移动阴影,舞台也已经关闭了景深。19
- 闪电闪光(第 5 项),以及每盏灯的闪烁,除非闪烁能通过检查 6,并在 Reduce Motion 下停止。12
- 《水晶》的调色板,《绿宝石》的雨、雾和水花精灵,以及它们的天气名称。 只取机制。1210
- 整点硬切,强制的
-plazaHour下除外。12
核心要点
如果你画美术
- 把夜晚画成一套调色板,而不是第二套图块。《水晶》保留大约一半的光(在它的调色板区块中为每个调色板白天亮度的 0.41 到 0.68,按小镇显示时各地图组平均为 0.49 到 0.54),夜里七个世界调色板中有五个共用同一种蓝灰色作为最亮的颜色,小镇的水只差一档。1
- 让早晨成为更暖的白色,仅此而已。《水晶》为早晨改变 32 种颜色中的 5 种,全都是最亮的那一种。1
- 给亮着的东西一种拒绝黑暗的颜色。《水晶》的 YELLOW 在其他一切都低于 0.6 时仍保持 0.90;Kiradex 的发光格为 0.901,而染色后最亮的像素是 0.562(在 Mac 上读取,不是在 iPhone 上测的)。19
- 让投影短、单一色调、落在一两个面上,并且无论高度如何都一样长;让它们偏向蓝色而不是黑色。86365
如果你写引擎
- 把人物排除在色调之外,或者大部分排除。《水晶》和《绿宝石》都这样做,用的是两种不同的机制。15
- 根据时钟计算光照并渐变;不要在人们开着不关的屏幕上硬切。30 分钟的渐变把 Kiradex 0.108 的硬切变成 0.0036 的步长(以图集平均亮度计,在 Mac 上读取)。11
- 让光照按房间的时钟运行,自己安排边界,并为手机的边界观察系统的时间跳变通知;不要等下一分钟。438384
- 加法光无法变暗。在 RealityKit 的两种混合模式下,移动的阴影是一张 alpha 片,固定的阴影属于图集。7866
- 在每个让空气动起来的文件里读取 Reduce Motion。869
如果你设计循环
- 把天气做成世界的一种排定属性,每天掷一次并事先可知,而不是玩家的真实天空。《绿宝石》和《火红》都没有一张室外地图生来就在下雨。517
- 用大约一秒让天气渐入,并保持温和:《绿宝石》的雨是光照的 13/16。5
- 不要闪电闪光。经典作品的雷暴能通过 W3C 的阈值;按我认为孩子们可能在小镇里长时间停留的前提,没有必要去试探它。446
- 决定室内是否跟随一天的变化。《水晶》对 208 张中的 207 张说不;《星露谷物语》说是;选一种并坚持下去。116
常见问题
《宝可梦 金》《银》《水晶》的昼夜循环是怎么运作的?
在本文所读的《水晶》中,实时时钟把一天分成从 4:00 开始的早晨、从 10:00 开始的白天和从 18:00 开始的夜晚,分别为 6、8、10 小时。每一段都有自己的一组八个背景调色板、八个对象调色板,以及室外的屋顶色,时段变化时,游戏在一帧之内重新载入它们,没有渐变。只有城镇、道路和一个洞穴跟随时钟:208 张室内地图中有 207 张固定在白天,27 张地图永远是夜晚,其中 24 张是 CAVE 地图。2134
《宝可梦 绿宝石》有昼夜循环吗?
光照上没有。《绿宝石》和《火红》的源码中都没有昼夜系统;《绿宝石》的时钟驱动树果和每日事件。《绿宝石》有的是 18 个天气值,按地图和触发器设置:其中包括雨、雷暴、暴雨、阴天、雾、火山灰、沙暴和大晴天,雨会把景物压暗到约 13/16,而人物保持本色。510
《宝可梦 绿宝石》的雨是怎么运作的?
雨是 10 个雨滴精灵,雷暴是 16 个,每滴以每秒 868 像素、偏离竖直 26.6 度的角度下落 18 帧,然后是 7 帧的水花;暴雨的 24 滴以每秒 1,336 像素下落 12 帧,然后是 10 帧的水花。调色板在 1.00 秒内渐变到约为原值 0.81 的颜色映射,玩家和镇民的除外;游戏中途从晴空进入雨天时,在设置好图集和 24 个雨滴精灵、并每 9 帧激活一滴之后,下雨的情况下雨滴在变化后 115 帧、即 1.9 秒时全部到位。没有一张室外地图的头部是雨:雨来自五张地图上的坐标触发器、少数脚本,以及 Route 119 和 123 的四天周期。57
不重画图块,怎样在像素游戏里做昼夜?
给每个时段的场景乘上一种色调,或者替换调色板,并把亮着的东西排除在外。《水晶》的夜晚保留大约一半的光并变蓝;Kiradex 乘以四种色调,把亮着的窗户画成不加色调的第二套网格,并在地面上放加法光池。在色调之间渐变而不是硬切,并让角色比场所更亮。196611
俯视像素艺术里的阴影该怎么画?
短,单一色调,放在单独的图层上。《绿宝石》的阴影精灵无论尺寸大小都是五行高的单色。Raymond Schlitter 让投影保持低调,放在单独的图块上,落在一两个面上,无论墙有多高都一样长;石井浩一用深蓝和紫色代替黑色。在只有加法和 alpha 混合的引擎中,固定的阴影属于美术本身。862636578
像素游戏应该使用玩家的真实天气吗?
经典作品的答案是不应该。《绿宝石》的天气属于它的地图和日子,《星露谷物语》每天为整个山谷掷一种天气,并在前一天通过电视公布。真实天气需要玩家的位置,会把一个共享的世界变成一份私人天气预报;以区域和日期为键的每日掷值,能让一个地方的每个人看到同一片天空,并且事先就知道。5178711
游戏里的闪电会有引发癫痫的风险吗?
闪光可能会。W3C 的成功准则 2.3.1 把标准定为任何一秒内不超过三次闪光,除非闪光低于一般闪光和红色闪光阈值,并指出闪烁的内容可能引起偏头痛、眩晕、恶心和癫痫发作。实测的《绿宝石》雷暴从不在一秒内出现两道闪电。Kiradex 的简报则完全不做闪光。464
本文是怎么做的。 背后的调研资料于 2026 年 10 月 5 日整理完成,来源包括四个 pret 反编译项目的浅克隆,由连同输出一起保存的五个 Python 脚本测量;还有保存下来的 Stardew Valley Wiki 关于一天、天气和地图的页面,八个关于现代带光照像素游戏的页面(访谈、一篇开发者文章、一份媒体资料、一篇新闻报道,以及 Wikipedia 的 HD-2D 条目),三篇 Pixelblog,一篇关于《圣剑传说 3》的翻译访谈,W3C 关于闪光的页面,以及由第六个脚本读取的十一个 Apple 文档页面。发布前我把它们全部重跑了一遍:两个原样返回了保存的输出;提议脚本改为逐行放置建筑阴影,按第 10 节的定义统计潮湿期,并从应用源码读取 Kiradex 的步行速度,而不是沿用每秒 4 个图块;《绿宝石》脚本的雨标签被修正,因为它曾把生成计数器的上限当作周期打印出来;《水晶》脚本新增了对渐变例程调用方的普查,它的地图头解析器被修正,因为它漏掉了五个音乐字段为表达式的头,现在它还会解析每种环境实际显示的调色板组;Kiradex 脚本的云那一行去掉了同样沿用的步行速度。每个改动过的脚本的早期版本和输出都保存在旁边。第七个脚本在纸面上重放简报的截图检查会如何框取小镇和一个房间,第八个 arith.py 打印每个派生数字,第九个把应用的颜色常量与《水晶》的调色板比较,第十个从源码逐次调用地重放《绿宝石》的雨天设置和雷暴周期,第十一个统计收藏者的帧中有多少被收藏者自身覆盖。正文中每个宝可梦数字都是某个脚本的输出,每条注释都点明了是哪个脚本。每个 Kiradex 数字都是在 Mac 上从源码读出或根据导出图片计算的;没有一个是在 iPhone 上截取的。第 10 节的提议是我的,并已如此标注;其中没有任何东西已经建成。
本站相关文章:在 iPhone 上造一个像素艺术世界:16 位时代的大师们早就懂的事是本系列的第一篇,包括 RealityKit 做法、光照分级表和法律界线;像素艺术建筑:在 iPhone 上盖房子、大厅与室内做出了亮着的窗户,并读过 roofs.pal 和 NightTiles;致敬老宝可梦游戏,而不借用它们对照《水晶》的钟点定下了 Kiradex 的四个时段;像素艺术人物:iPhone 上的角色与捏人器测量了雨的尺寸所依据的视图;像素艺术的运动:iPhone 上的行走、相机与门给出了用来衡量空气的步行速度;无人在线时依然鲜活的像素小镇:NPC 与幽灵提议了光照将要跟随的房间时钟;像素艺术房间:家具网格、上限与评分就是第 8 项要按钟点点亮其窗户的那个房间;像素艺术的道路:iPhone 上的地图连接与街区是广场之外的世界,如果建成,每日天气也会覆盖它;像素艺术的台阶、楼梯与桥:用图块搭出世界的高度测量了跳跃时留在地面上的阴影,并引用了 Sabotage Studio 的 Thierry Boulanger 关于在光照中读出高度的话。
参考来源
-
作者测量,2026年10月5日:
measure_gen2_daylight.py,位于作者为本文准备的证据文件夹,输出gen2_daylight.txt(以及gen2_yellow_tiles.png,三个图块的 4 倍渲染,源自受版权保护的美术,留作分析,不予发布)。各部分:A,钟点(constants/misc_constants.asm和engine/rtc/rtc.asm中的TimesOfDay表);B,gfx/tilesets/bg_tiles.pal中按时段的背景调色板,按文件中的区块原样计算(早晨、白天、夜晚、黑暗和室内时各调色板四种颜色的平均亮度;夜昼之比;各时段的颜色 0;早晨改变的颜色;蓝色减去红绿平均值;夜晚或黑暗中亮度高于 0.6 的颜色);B2,地图实际显示的调色板组:data/maps/environment_colors.asm中每种环境和时段指向文件 42 个调色板的八个索引,室外组的 WATER 取自室外那一对,以及LoadMapPals按地图组从gfx/tilesets/roofs.pal复制屋顶色(室外组的亮度及夜昼之比;显示的水、它夜间最亮的颜色及其比值;共用 (15, 14, 24) 的五个调色板;早晨与白天的对比;蓝色偏向;高于 0.6 的颜色;含有城镇或道路的 25 个地图组、显示的 ROOF 在这些组上的比值,以及七个世界调色板按组的平均比值;室内组);C,gfx/overworld/npc_sprites.pal中的对象调色板(从白天到夜晚和黑暗哪些颜色改变);D,gfx/tilesets/roofs.pal(27 个条目,按地图组的夜昼之比);E,engine/tilesets/timeofday_pals.asm中的_TimeOfDayPals、_UpdateTimePals和四种渐变、大地图循环、对各渐变调用方的普查(data/maps/setup_scripts.asm中每个地图设置脚本的渐变,对没有db -1的脚本继续跟进到下一个;踩上的传送点、飞翔和脚本命令warp用哪个设置常量载入;home/map.asm中的菜单渐变;timeofday_pals.asm之外任何.asm文件中对渐变的每次special、farcall或mapsetup,按文件统计,以及黑色那一对的十一个地图脚本及其行号)、背景调色板由LoadSpecialMapPalette从LoadMapPals手中接管的六个图块集(按地图头的环境统计),以及确实由它载入调色板的地图(位于TILESET_ICE_PATH上的 INDOOR 地图殿堂除外:74 张 INDOOR 地图,全部为PALETTE_DAY,其中 57 张在TILESET_HOUSE上,另有 5 张 CAVE),开始菜单自己的代码行(StartMenu、它的.Exit、它对UpdateTimePals的调用、它的哪些菜单项调用FadeToMenu),以及mobile/和home/map.asm之外的每次call FadeToMenu,按文件统计;F,data/maps/maps.asm中的 388 个地图头,按环境和调色板模式统计,每条map指令按逗号拆分,并断言每条指令都被完整消费(早期的解析器匹配八个单词字段,漏掉了音乐字段是表达式的五个广播塔头,所以打印出 383 个头、203 张 INDOOR 地图中 202 张为白天、383 张中 78 张跟随时钟,20.4%;现在是 388、208 张中 207 张、388 张中 78 张,20.1%);G,城都图块集的调色板映射gfx/tilesets/johto_palette_map.asm。亮度为对缩放到 0 到 1 的 5 位值应用 Rec. 601,不做伽马校正。输入:pretpokecrystal,5beda23。发布前重跑:A 到 D、G 部分以及图块渲染与之前相同;E 部分新增了渐变普查、菜单普查和使用特殊表的地图计数,B2 和 F 的修正解析器是在之后的修订中加入的,早期脚本和输出保存在旁边。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokecrystal/constants/misc_constants.asm,5beda23(MORN_HOUR EQU 4、DAY_HOUR EQU 10、NITE_HOUR EQU 18,第 36 到 38 行)以及pokecrystal/engine/rtc/rtc.asm(TimesOfDay,第 48 行,第 50 行注释为“0400-0959 morn | 1000-1759 day | 1800-0359 nite”),2026年10月5日阅读,https://github.com/pret/pokecrystal/blob/master/engine/rtc/rtc.asm ↩↩↩↩↩ -
pret,
pokecrystal/engine/tilesets/timeofday_pals.asm,5beda23,2026年10月5日阅读:_TimeOfDayPals(第 13 行)、_UpdateTimePals(第 109 行,ld c, $9 ; normal,然后是GetTimePalFade和DmgToCgbTimePals)、FadeInFromWhite(115)、FadeOutToWhite(122)、FadeInFromBlack(146)、FadeOutToBlack(153),以及ReplaceTimeOfDayPals(189,在第 206 行检测STATUSFLAGS_FLASH_F),https://github.com/pret/pokecrystal/blob/master/engine/tilesets/timeofday_pals.asm ↩↩↩↩↩ -
作者根据正文中的测量值所做的算术:
arith.py,位于作者为本文准备的工作文件夹,输出arith.txt,读取作者为本文准备的证据文件夹中的输出,并只读地读取 Kiradex 导出的town.png和glow.png。它打印:帧率(2^24 ÷ 280,896 和 2^22 ÷ 70,224,均为 59.7275 赫兹);钟点及其在一天中的占比(夜晚 24 中占 10,41.7%;Kiradex 的路灯 13 小时,54.2%);《星露谷物语》的一天(20 × 60 × 0.7 = 840 秒,14 分钟;入夜在早上 6 点后现实 9.8、9.1 和 8.4 分钟);《水晶》各调色板的夜昼之比及其平均值(0.511)、蓝色偏向的变化(6.44,31 的 0.208)、颜色 (15, 14, 24) 的亮度 0.498、(30, 30, 11) 的 0.898、(27, 31, 27) 的 0.947 和 (28, 31, 16) 的 0.916,取自gen2_daylight.txt第 B2 部分的室外显示水面(白天 0.528,夜晚 0.248,0.470;它夜间最亮的颜色 (15, 13, 27),0.490),按小镇显示时七个世界调色板按地图组的夜昼之比(0.494 到 0.539,25 组平均 0.509),显示的 ROOF 的比值(0.369 到 0.688,平均 0.477)以及显示时的蓝色偏向(−1.17 到 +6.10,上升 7.27),早晨的 32 中 5(15.6%),YELLOW 的 192 个图块中 3 个(1.6%),重载的 64 + 64 + 4 = 132 字节,渐变的 8 帧(134 毫秒),从 F 部分解析的地图头(388;跟随时钟的地图 388 张中 78 张,20.1%;固定为白天的室内 208 张中 207 张;27 张永夜地图按环境统计,CAVE 24、DUNGEON 1、GATE 1、INDOOR 1;早期的 383、383 中 78 和 203 中 202 记在旁边);《绿宝石》的 13/16(0.8125),映射 3 在 31、16 和 8 处的值,雨的渐变(60 帧,1.005 秒),雨和暴雨的速度、下落和水花(262 和 268 像素,0.301 和 0.201 秒,7 和 10 帧,0.117 和 0.167 秒,一滴雨的寿命 25 帧,0.419 秒),生成周期(延迟 + 1:9 和 5 帧),从零个激活雨滴起的激活阶段(90、80 和 120 帧,1.51、1.34 和 2.01 秒),以及来自gen3_replay.txt的、游戏中途从晴空开始的完整转换(25 次设置调用 + 激活:115、105 和 145 帧,1.93、1.76 和 2.43 秒;调色板在第 59 帧到达映射 3;雷暴的第一道闪电在第 109 帧;雨到雨的计数),云、雾和沙暴相对每秒 59.73 像素步行的比值(0.500、0.250、3.60),雷暴的等待、持续和间隔(以秒计),暗间隔为间隔计时器加一(61 到 76 帧,1.021 到 1.272 秒;61 帧为 1.0213 秒,闪电起始至少相隔 67 帧),一个周期第一次落雷前的等待(361 到 720 帧),雷暴图的时间线,以及雷声相对落雷的延迟(1 到 20 帧,最多 0.335 秒;1 到 100,最多 1.674);室外头部的占比(68 中 51,75.0%;76 中 66,86.8%)和 86 个坐标事件;闪光圆(1,810 和 16,286 平方像素,占 38,400 的 4.7% 和 42.4%,面积九倍);阴影精灵的不透明占比,以及 Kiradex 的(48 中 40,alpha 153 = 0.60,亮度 0.071);Kiradex 图集在各色调下的值(0.4318、0.4488、0.3719、0.2639),发光格与染色后最亮像素之比(1.60),四次硬切(0.0170、0.0769、0.1079、0.1678),以及每个边界上 30 分钟渐变的最大每分钟步长(0.0006、0.0026、0.0036、0.0056);完整叠加色调时路灯光池的中心(0.806)以及按图片峰值 0.961 加权后(0.785);7 + 17 = 24 个光池;从Motion.swift读取的如今的步行(每秒 60 刻度下 16 刻度走 16 像素,每秒 60 像素;跑步 120),云的每秒 1.640 像素(步行的 0.0273,每个图块 9.8 秒,比 16 慢 9.75 倍),提议的每秒 16 像素与步行之比(0.2667),走过与视图高度相等的距离需 22.4 秒,以及根据摘要第五个字节重算的 2027 年 1 月 1 日方向(153,215.15625 度;竖直分量每秒 9.21293 像素,38.86 秒穿过 358 像素高度,水平分量 13.08136,12.61 秒穿过 165 像素宽度),它在不透明度 0.55 和 0.8 时的阴影(最深 0.763 和 0.655;平均 0.950 和 0.927)以及阴天的白天(0.404);59,070 平方像素的视图(1.538 个屏幕)及其中的雨滴;图集上的雨和大雨(0.365、0.214;0.337、0.198);各时段行走者提亮后的光(0.980、1.000、0.913、0.796;夜里是场所的 1.34 倍);天气年的潮湿占比;Reduce Motion 下的三分之一和一半(5 滴,12.3,每秒 434 像素);建筑阴影的 33 × 4 × 16 = 2,112 像素(28 格草坪、5 格小路,6 行因道具跳过)以及底部以下的 40 格,其中 35 格是地面;进入雨和大雨的每帧渐变(作为硬切为 0.084 和 0.112;60 赫兹时每帧 0.0014 和 0.0019,120 赫兹时 0.0007 和 0.0009);一秒渐变所分配的场所材质,从代码统计(一次重新打光 3 次,水波每秒 12 次,渐变期间 60 赫兹 180 次、120 赫兹 360 次;显示门时,一次重新打光 4 次,渐变期间 240 和 480 次,这一行于 2026 年 10 月 16 日加入);天气年的湿天和潮湿期(82 天,17 + 19 + 21 + 25;65 个潮湿期,最长 3 天);Celeste 的 (2048 ÷ 256)^2 = 64 个槽位 × 4 = 256;以及保存的MaterialParameterTypes.BlendMode页面的两个枚举值,从其主题列表读出,parse_apple.py不打印它们。修订内容:改为从脚本读取头部计数而不是沿用,增加显示的调色板、完整的雨天转换、雷暴间隔以及云的比值和穿越时间,并改为从应用的Motion.swift读取如今的步行而不是沿用每秒 64 像素;早期脚本和输出保存在旁边。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
作者测量,2026年10月5日:
measure_gen3_weather.py,位于作者为本文准备的证据文件夹,输出gen3_weather.txt。各部分:A,include/constants/weather.h的常量;B,src/field_weather_effect.c中每种天气的InitVars和混合目标;C,用 Python 重放src/field_weather.c中的BuildColorMaps、sBasePaletteColorMapTypes,以及include/event_object_movement.h的调色板槽位;D,雨的 Q28.4 速度、下落时长、水花帧和生成周期(UpdateVisibleRainSprites的++rainSpriteVisibleCounter > rainSpriteVisibleDelay,每延迟 + 1 帧一滴,以及Rain_InitAll在一帧内的循环);E,云、雾和沙暴的漂移(沙暴对照src/trig.c中的gSineTable重放 6,000 帧);F,雷暴计时;G,《绿宝石》和《火红》每个data/maps/*/map.json的头部天气按地图类型统计、坐标天气事件、两个道路周期、data/**/*.inc中的脚本setweather调用,以及requires_flash地图;H,sFlashLevelToRadius和默认闪光等级;I,在src/*.c和include/**/*.h中不区分大小写地搜索名称“timeofday”和“time_of_day”。输入:pretpokeemerald,731ad5b,以及pokefirered,037335f。发布前重跑:除 D 部分最后几行外,各部分与之前相同;那几行的标签曾把rainSpriteVisibleDelay本身当作生成周期打印(每 8 和 4 帧),现在打印的是延迟 + 1(每 9 和 5 帧)以及填满所需时间;早期脚本和输出保存在旁边。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/src/field_weather.c,731ad5b,2026年10月5日阅读:sWeatherFuncs(第 85 行,每种天气的initVars、main、initAll和finish)、sBasePaletteColorMapTypes[32](第 114 行)、StartWeather(154)、SetNextWeather(183)、Task_WeatherInit(216,屏幕就绪后在第 222 行调用天气的initAll)、Task_WeatherMain(227:当前天气与下一天气不同时,每帧调用旧天气的finish()直到它返回 false,在第 231 行,然后在第 235 行调用新天气的initVars(),第 236 行colorMapStepCounter = 0,第 237 行WEATHER_PAL_STATE_CHANGING_WEATHER;否则在第 244 行调用天气的main();两种情况之后都在第 247 行调用调色板状态函数)、BuildColorMaps(271)、UpdateWeatherColorMap(344,在第 354 行当++colorMapStepCounter >= colorMapStepDelay时推进颜色映射)以及ApplyColorMap(459);还有include/constants/field_weather.h第 4 行,MAX_RAIN_SPRITES 24,https://github.com/pret/pokeemerald/blob/master/src/field_weather.c ↩↩↩↩↩↩ -
pret,
pokeemerald/src/field_weather_effect.c,731ad5b,2026年10月5日阅读:云坐标的注释“These coordinates are for the lower half of Route 120.”(这些坐标用于 Route 120 的下半部分。)(第 41 行)、Clouds_InitVars(95)、UpdateCloudSprite的注释“Move 1 pixel left every 2 frames.”(每 2 帧向左移 1 像素。)(222)、sRainSpriteSplashAnimCmd(414)、sRainSpriteMovement(449)、sRainSpriteFallingDurations(458)、Rain_InitVars(471)、Rain_InitAll(484,循环直到weatherGfxLoaded)、Rain_Main(491:它的三个状态,第 496 行的图集,第 500 行的CreateRainSprite直到返回 false,第 504 行的UpdateVisibleRainSprites直到返回 false)、Rain_Finish(513,下一天气是雨类时立即返回 false 并保留精灵,518 到 524)、CreateRainSprite(665:已有 24 个时返回 false,670 到 671,每次调用创建一个精灵,并在创建第 24 个的那次调用返回 false,694 到 709)、UpdateVisibleRainSprites(714,激活数达到目标时返回 false,716 到 717,并在++rainSpriteVisibleCounter > rainSpriteVisibleDelay时激活一滴雨,第 719 行)、Sunny_Finish(168,返回 false)、雷暴状态的枚举(1014 到 1035)、Thunderstorm_InitVars(1037)、Downpour_InitVars(1066)、雷暴模式的注释“(SHORT_BOLT){1,2}(LONG_BOLT)?”(1089)以及“Thunder only plays on the final bolt of the cycle.”(雷声只在周期的最后一道闪电时播放。)(1091)、Thunderstorm_Main(1092),首先调用UpdateThunderSound()(1094),它的设置状态(1097 到 1111)以及它的THUNDER_STATE_WAIT_CHANGE分支(1112),该分支跳过等待直接跳到THUNDER_STATE_INIT_CYCLE_1(1114)、THUNDER_STATE_NEW_CYCLE(1116,第 1118 行的计时器(Random() % 360) + 360落入第 1121 行的THUNDER_STATE_NEW_CYCLE_WAIT,其--thunderTimer == 0在第 1123 行)、THUNDER_STATE_INIT_CYCLE_1和_2(1126 和 1131,落入第 1135 行的THUNDER_STATE_SHORT_BOLT,第 1137 行落雷,第 1142 行持续(Random() % 3) + 6)、THUNDER_STATE_TRY_NEW_BOLT(1145,第 1149 行闪电结束,第 1154 行间隔计时器(Random() % 16) + 60)、THUNDER_STATE_WAIT_BOLT_SHORT(1169)、THUNDER_STATE_INIT_BOLT_LONG(1173,第 1174 行同样的间隔计时器)、THUNDER_STATE_WAIT_BOLT_LONG(1177,第 1181 行EnqueueThunder(100),第 1182 行落雷,第 1183 行持续(Random() & 0xF) + 30)、THUNDER_STATE_FADE_BOLT_LONG(1187,第 1191 行逐渐渐变)、EnqueueThunder(1242,第 1246 行计时器Random() % waitFrames)和UpdateThunderSound(1251)、FogHorizontal_Main(1392)、Sandstorm_Main(1975)、Shade_InitVars(2247)、sWeatherCycleRoute119(2581)、sWeatherCycleRoute123(2588)以及UpdateWeatherPerDay(2622),https://github.com/pret/pokeemerald/blob/master/src/field_weather_effect.c ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
作者测量,2026年10月5日:
measure_shadows.py,位于作者为本文准备的证据文件夹,输出shadows.txt:《绿宝石》的shadow_small.png、shadow_medium.png、shadow_large.png和shadow_extra_large.png(pretpokeemerald,731ad5b,graphics/field_effects/pics/)以及 Kiradex 的Kiradex/Resources/World/shadow.png的尺寸、不透明像素和不同色调数,每个都画成其不透明像素的文本遮罩。发布前重跑,输出相同。 ↩↩↩↩↩↩↩↩↩↩ -
作者测量,2026年10月5日,在 Mac 上而不是 iPhone 上:
measure_kiradex_light.py,位于作者为本文准备的证据文件夹,输出kiradex_light.txt。它以文本方式读取 Kiradex 代码库的Kiradex/World/Daylight.swift、PlazaStage.swift、Air.swift以及Kiradex/World/中的每个 Swift 文件,还有scripts/forge/town.py和buildings.py;并用 Pillow 读取导出的Kiradex/Resources/World/town.png(把色调逐通道应用到它的不透明像素上)、cloud.png、mist.png、glow.png、shadow.png和town.json(40×30,日期 2026 年 10 月 3 日 21:34)。各部分:A,Daylight 的时段、色调、色偏和路灯;B,图集上的色调和发光格;C,时钟、它的观察者和 Reduce Motion;D,空气;E,小镇的光源、发光格、光池、烟囱和建筑;F,阴影。亮度为对 0 到 1 的值应用 Rec. 601,不做伽马校正。私有代码库,10 月 5 日 16:54 在提交cf3beb1e时、从当天正在编辑的工作树中读取,没有运行 git 或锻炉。发布前于 18:11 在提交66c116b0时重跑:除 D 部分有一行外,每一行与之前相同;那一行的标签曾沿用每秒 4 个图块的步行速度,而该行的数字,即云穿过一个图块所需的 9.8 秒,并不使用它;早期脚本和输出保存在旁边。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Blake Crosley,“Honoring the Old Pokémon Games Without Borrowing Them”,blakecrosley.com,2026年10月5日:第 3 节(《水晶》的
MORN_HOUR 4、DAY_HOUR 10和NITE_HOUR 18以及TimesOfDay的注释;Kiradex 的Daylight.Phase,“morning from 6 to 9, day from 9 to 17, evening from 17 to 20 and night otherwise”(6 点到 9 点为早晨,9 点到 17 点为白天,17 点到 20 点为傍晚,其余为夜晚),以及“the brief in section 14 keeps it that way”(第 14 节的简报保持这样)的表述;《绿宝石》按实时时钟生长的树果,以及它列出的《绿宝石》十一项每日更新,其中有“the weather”(天气),但没有点出函数)和第 14 节(简报,保留 Kiradex 的时段,“which are already not Crystal’s 4, 10 and 18 and stay that way”(它们已经不是《水晶》的 4、10 和 18,并将保持如此);以及它的“Never”列表,其第 1 项拒绝任何“tile, sprite, palette or screen layout taken, traced or redrawn from any Pokémon game”(取自、描摹或重绘自任何宝可梦游戏的图块、精灵、调色板或屏幕布局),第 2 项点名任何功能都不得使用的系列词汇,从 Pokédex 和“Poké-anything”(任何 Poké 开头的东西)到 Frontier,第 4 项拒绝在应用或锻炉中使用“code or data from the decompilations”(来自反编译的代码或数据)),https://blakecrosley.com/blog/honoring-the-old-pokemon-games ↩↩↩↩↩↩↩↩↩↩↩↩↩ -
作者的测量与提议算术,2026年10月5日,在 Mac 上:
measure_kiradex_proposals.py,位于作者为本文准备的证据文件夹,输出kiradex_proposals.txt。各部分:1,把《绿宝石》的雨换算到 165×358 世界像素的 iPhone 18 Pro Max 视图(每 10,000 平方像素的密度、视图中的雨滴、60 和 120 赫兹下的每帧速度、下落和水花时间);2,从 17:18 的Motion.swift副本读出的 Kiradex 步行(每秒 60 刻度下 16 刻度走 16 像素,每秒 60 像素;跑步 120;斜向步行一步沿对角线 59.0),各游戏步行速度下的漂移速度(《绿宝石》每秒 59.7 像素,Kiradex 60),以及提议的每秒 16 像素和半步速的云;3,town.json中建筑阴影的格子:对七座建筑每一行,紧挨该行最东侧建筑格东边的那一格,按字母分类,仅在草坪、小路或铺石处加阴影(39 中 33),并作对比的每列最低建筑格下方的格子(40 格,其中 35 格是地面);4,白天到傍晚、傍晚到夜晚在图集平均亮度上的硬切和 30 分钟渐变;5,图集上雨的 13/16,以及行走者向白色提亮一半的光;6,一个区域内提议的每日天气掷值一年的结果(2027),sha256("zone|yyyy-mm-dd"),前四个字节大端,对 100 取模,按列出顺序累积对照提议的季节表(它打印每个季节各状态的掷值范围),湿天和潮湿期(雨、大雨或雪的天,任意混合的连续天数的最大段:82 天,65 个潮湿期,最长 3 天),两个区域在 1 月 30 天上的一致情况,以及根据摘要第五个字节提议的云的方向。输出中标为 PROPOSAL 的都是作者的。第 3 部分从每座建筑的包围盒改为上述逐行规则。第 6 部分经过修订:它曾统计同一种潮湿状态的连续段(67,最长 3),并漏掉一段以年末最后一天结束的连续段;现在按上文定义统计潮湿期,并打印掷值范围和云的方向。于 18:11 在提交66c116b0时重跑,读取工作树中的小镇导出数据和图片。第 2 部分改为从Motion.swift读取步行速度:它曾沿用每秒 4 个图块,即每秒 64 像素,因而把如今的云算成步行的 0.026,把提议算成 0.25。早期脚本和输出保存在旁边,第 1、3、4、5 部分以及第 6 部分的季节计数与之前相同。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
作者为本文准备的调研资料,“Light, weather and time: how pixel worlds keep a day, and what Kiradex should”(光、天气与时间:像素世界如何保持一天,以及 Kiradex 应该怎么做),2026年10月5日整理;Kiradex 代码库
docs/research/light/01-light.md(私有;当天 07:25 的日期,16:51 在提交cf3beb1e时读取),与作者证据文件夹中的副本相同。第 1 节(关键事实)、第 2 节(案例研究)、第 3 节(带数字的规则)、第 4 节(给 Kiradex 的简报,第 10 节的状态、表格、系数、文件和验收检查 1 到 8,以及本文的 10 和 11(调研资料的 9 和 10)都按最初的写法出自这里;本文修订了检查 2 到 8、10 和 11,并在每处说明;它的检查 10 说天气图片要通过“the deny list”(拒绝列表),它的云的方向来自“the weather roll’s second byte”(天气掷值的第二个字节))和第 5 节(测了什么、没测什么:近似、未能访问的页面、提议,以及与已发布文章的核对)。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokecrystal/gfx/tilesets/bg_tiles.pal,5beda23,2026年10月5日阅读:五个各含八个背景调色板的区块(早晨、白天、夜晚、黑暗、室内)以及一对室外水面调色板(第 51 到 53 行:RGB 23,23,31, 18,19,31, 13,12,31, 07,07,07 ; morn/day和RGB 15,13,27, 10,09,20, 04,03,18, 00,00,00 ; nite),https://github.com/pret/pokecrystal/blob/master/gfx/tilesets/bg_tiles.pal ↩↩ -
pret,
pokecrystal/gfx/overworld/npc_sprites.pal,5beda23,2026年10月5日阅读:按时段的对象调色板,https://github.com/pret/pokecrystal/blob/master/gfx/overworld/npc_sprites.pal ↩↩ -
pret,
pokeemerald/include/event_object_movement.h,731ad5b,2026年10月5日阅读:从PALSLOT_PLAYER(第 12 行)到PALSLOT_NPC_SPECIAL(第 22 行)的对象调色板槽位及其倒影槽位,https://github.com/pret/pokeemerald/blob/master/include/event_object_movement.h ↩↩ -
Stardew Valley Wiki,“Day Cycle”,2026年10月5日保存为作者为本文准备的证据文件夹中的
sdv-day-cycle.html和sdv-day-cycle.txt(“Time passes at 0.7 real seconds per in-game minute”(游戏内每分钟相当于现实 0.7 秒);入夜“In Spring and Summer , it is 8pm, while in Fall it is at 7pm and Winter is 6pm”(春夏为晚上 8 点,秋季为 7 点,冬季为 6 点);“indoors the windows turn dark and lamps and sconces light up”(室内窗户变暗,灯和壁灯亮起);室外“it does not immediately become full dark, but gradually becomes darker over time”(不会立刻全黑,而是随时间逐渐变暗)),https://stardewvalleywiki.com/Day_Cycle ↩↩↩↩↩↩↩↩↩ -
Stardew Valley Wiki,“Weather”,2026年10月5日保存为作者为本文准备的证据文件夹中的
sdv-weather.html和sdv-weather.txt(“Weather changes daily and with the seasons”(天气每天、随季节变化);第二天的天气“can be checked by viewing the TV screen”(可以通过看电视屏幕查看);“There is a chance for a lightning strike to occur every 10 minutes”(每 10 分钟有一定概率发生雷击);“Snow transparency can be changed in the Options Menu”(雪的透明度可以在选项菜单中更改)),https://stardewvalleywiki.com/Weather ↩↩↩↩↩↩↩↩↩ -
Stardew Valley Wiki,“Modding:Weather data”,2026年10月5日保存为作者为本文准备的证据文件夹中的
sdv-weather-data.html和sdv-weather-data.txt(没有强制天气的日期按季节的各天气类型概率),https://stardewvalleywiki.com/Modding:Weather_data ↩↩↩↩↩↩↩↩ -
Kiradex 源码,
Kiradex/World/PlazaStage.swift,2026年10月5日阅读,最后一次是 17:42 在提交5a344e3f时,取自应用自己的代理当天正在编辑的工作树(私有代码库;文件日期为当天 17:22,行号以那次读取为准;16:51 在提交cf3beb1e时的读取中,第 469 行之后的行号要小 4 到 8,因为文件之后加入了一个无障碍表示):daylight(at:)(第 69 行,设置时使用Daylight.forcedHour)、relight()(77 到 81,在第 152 行构建之后和第 194 行空气准备之后调用)、stepTrace,即玩家的最后一步或空气的报告,“as the stage’s accessibility value”(作为舞台的无障碍值)(60 到 63,在第 361 行由StageAccessibility交给舞台的无障碍元素)、content.renderingEffects.depthOfField = .disabled(133)、在视图的.task中、rig.air.prepare()之后写入舞台值一次的-airProbe报告(190 到 193)、调用relight()的onChange(of: clock)(248 到 250)、休眠 60 秒的任务“The light follows the clock, a minute at a time.”(光跟随时钟,一分钟一次。)(251 到 257)、扫描线,每三个点一条阴影线(344 到 349)、暗角,一个从透明经透明到不透明度 0.28 的黑色、半径 900 点的径向渐变(353 到 355)、RoomView(第 911 行声明,在第 946 行构建一个PlazaStage,第 975 行fitsWhole: false);时钟@State(57);PlazaView.words,状态栏的文字(819);每帧推进rig.update的SceneEvents.Update订阅(176 到 182);把stepTrace设为玩家步伐的rig.onStep(156 到 160);bubbles(of:in:),头顶的对话气泡,位于TimelineView(.animation(minimumInterval: 0.1))之下(447 到 463);以及PlazaView(536),它以表单形式呈现QuestBoardView(690),并把房间或大厅叠在广场上,“laid over it, not pushed”(叠在上面,而不是推入),广场留在那个地方之下(719 到 724);RoomView的“A room is entered through the veil, and its floors change under it.”(经由帷幕进入房间,它的楼层在帷幕下切换。)(936),以及它client: nil的PlazaStage(947);以及广场从房间、展会或拜访中返回,“out of the door gone in by, the veil lifting.”(从进去的那扇门出来,帷幕揭开。)(678 到 679)。那次读取时,Kiradex/World/中没有significantTimeChangeNotification或NSSystemTimeZoneDidChange的观察者。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Kiradex 源码,
Kiradex/World/PlazaRig.swift,2026年10月5日 16:51 在提交cf3beb1e时、从当天正在编辑的工作树中读取(私有代码库;文件日期为 15:22,行号以那次读取为准):注释“For the card alone: nothing else in the world is lit.”(只为卡牌:世界上其他东西都不受光照。)下的卡牌DirectionalLight(第 161 和 162 行)、绑定层的light(211)、light(_:lampsOn:)(254)及其关于行走者“redrawn in the new light on the next frame”(下一帧在新的光下重绘)的注释(262)、使用material(light: .white)的亮窗网格(383),以及用绑定层的light从sheet.material(column:row:light:)得到的每个行走者帧的材质(512,在第 508 行的add(other:figure:tile:facing:)中,以及 1081,在animate(_:)中);animate(_:)本身(1038),站立的行走者在其中显示呼吸待机、眨眼,以及不时出现的idleEmotes之一,即抽动、张望、耸肩、思考和打盹(136),下一个在 6 到 14 秒之后(1062),并把材质显示的那一帧记录在行走者的shown中(1078 到 1080),即朝向所在行乘以图表的列数再加上画面所在列;玩家的第一个待机表情在 7 秒,第一次眨眼在 2.5 秒(Walker,33 到 35);在light(_:lampsOn:)中被赋予material(light:)的地面、周边和物件(257 到 259),以及在水波中每次帧变化时再次赋予,有水的地方每秒四次(231,240 到 250);showDoor(at:sheet:column:)中用绑定层的光绘制的门(617;材质在 627 和 634);build(tiles:collector:arrival:)(362),它首先用绑定层的光给地面、周边和物件赋予material(light:)(363、370 和 376);addQuad,平铺在若干图块上的一张图片(346);worldPoint(at:in:displayScale:)(459);viewPoint(of:in:displayScale:),一个世界点落在视图中的位置(471);update(_:),逐帧推进,它通过刻度时钟运行世界的刻度,然后转动展示柜、推进水波并更新空气(952 到 966),其中tick()(969)推进行走者和镜头;place(_:),把行走者的脚放在其图块中心下方 0.3 个图块处,取整像素(1088 到 1097);以及lockCamera()(1171 到 1200),它在CameraLock.bounds(Kiradex/World/Motion.swift,第 258 到 262 行,日期 12:36)向内取整到地图边缘的范围内,把镜头放在玩家身上。早先一次读取为镜头引用过的follow(_:),已不在这个文件中。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Kiradex 源码,
Kiradex/World/Daylight.swift,日期 2026 年 10 月 2 日,10 月 5 日 16:51 在提交cf3beb1e时、从当天正在编辑的工作树中读取(私有代码库;行号以该副本为准):Phase.init(hour:)(第 13 行)、tint(第 26 行,“a multiplier on the art’s own colours”(作用于美术自身颜色的乘数),声明为“ART SURFACE: four fixed lights for a pixel world”(美术层面:为像素世界准备的四种固定光))、lampsOn(第 36 行)、Season.init(month:)(第 54 行,“By the month, in the north; the south is a setting for later.”(按月份,以北半球为准;南半球留待以后做成设置。))、cast(第 75 行)、带calendar: Calendar = .current的now(_:calendar:)(第 86 行)、light(phase:season:)(第 92 行),以及从-plazaHour读取的forcedHour(第 101 行)。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Kiradex 源码,
Kiradex/World/Air.swift,日期 2026 年 10 月 3 日,10 月 5 日 16:51 在提交cf3beb1e时、从当天正在编辑的工作树中读取(私有代码库;行号以该副本为准):头部注释,“Light and air over a place, build 29 of the design plan”(场所之上的光与空气,设计计划第 29 版构建),结尾是“none of it takes the day’s tint: light is light.”(这些都不受当天色调影响:光就是光。)(第 6 到 12 行);glow 程序的描述符,带lightingModel = .unlit和blendMode = .add,以及注释“The program says how to blend; the material says that it blends at all.”(程序说明如何混合;材质说明是否混合。)(第 88 到 96 行);光池(第 144 到 153 行);set(phase:season:)(从第 160 行起:云在夜里隐藏,早晨有雾,秋天除夜晚外每个时段都有落叶,第 168 行,update第 276 行有同样的判断,夜里有萤火虫);makeCloud(第 175 行,不透明度 0.55,“repeats every 384 world pixels”(每 384 个世界像素重复一次));makeMist(第 192 行,不透明度 0.7,三团);14 片落叶(第 209 行);10 只萤火虫(第 248 行);-airProbe显示的报告(在第 105 和 114 行构建);逐帧的云偏移,cloudOffset += SIMD2(0.004, 0.0015) * dt加上camera / 384(第 266 到 268 行);深度,cloudDepth2.4、driftDepth2.45 和poolDepth2.5(第 19 到 21 行),落叶、萤火虫和烟位于driftDepth(237、251、287 和 314);TileMap.depth(atWorldY:),1 加上沿地图向下最多 0.5(Kiradex/World/TileMap.swift第 122 行);outdoors,场所是否有天空(第 27 和 71 行);云在夜里隐藏(第 163 行);作为漂浮物的落叶(第 207 到 215 行);以及update(_:camera:view:)(第 262 行)。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩ -
Blake Crosley,“Pixel-Art Structures: Houses, Halls and Interiors on iPhone”,blakecrosley.com,2026年10月3日:TL;DR 中的“nineteen windows and the hall’s two sconces”(十九扇窗户和大厅的两盏壁灯);关于亮窗作为不加色调的第二套网格、在路灯亮起时启用的一节;图集那一段,“the 21 lit cells dedupe to two”(21 个发光格去重后只剩两个),以及它关于两套新网格的 GPU 开销“is not profiled yet”(尚未做性能分析)的说明;《水晶》的
roofs.pal;以及《星露谷物语》的NightTiles和WindowLight,https://blakecrosley.com/blog/pixel-art-structures-on-iphone ↩↩↩↩↩↩↩↩ -
Kiradex 锻炉,
scripts/forge/buildings.py,日期 2026 年 10 月 3 日,10 月 5 日 16:51 在提交cf3beb1e时、从当天正在编辑的工作树中读取(私有代码库;行号以该副本为准):模块 docstring,“The ground shadow stays off the building: it belongs to the ground layer.”(地面阴影不放在建筑上:它属于地面层。)(第 13 到 16 行);放在山墙三角形中的窗户,当形状为gable且宽度至少为 5 时,在屋顶块正中放置K.window(lit=night)(第 91 到 93 行),记录在建筑的窗户中(112 和 113),两座山墙屋顶的建筑都有;dormer,一个没有发光变体、不记为窗户的屋顶部件(第 133 和 134 行);以及finish中屋顶下方三行的屋檐阴影(第 195 到 198 行)。 ↩↩↩↩ -
pret 反编译项目,作者为本系列准备的证据文件夹中的浅克隆:
pokecrystal,5beda23(2026 年 9 月 29 日提交),pokeemerald,731ad5b(10 月 1 日),pokefirered,037335f(9 月 26 日),以及pokered,d2704a6(9 月 22 日),https://github.com/pret ↩ -
作者读取,2026年10月5日:
parse_apple.py,位于作者为本文准备的证据文件夹,输出apple_pages.txt,打印当天保存的十一个 Apple 文档 JSON 页面(apple-*.html)各自的标题、摘要、声明和平台。发布前重跑,输出相同。 ↩↩↩ -
作者测量,2026年10月5日,在 Mac 上:
measure_capture_walk.py,位于作者为本文准备的证据文件夹,输出capture_walk.txt。这是在纸面上重放 Pro Max 镜头停在哪里,而不是在模拟器上运行,使用从 Kiradex 源码读出的绑定层自身几何(10 月 5 日 16:51,提交cf3beb1e,当天正在编辑的工作树;行号以那次读取为准):3 倍、每纹素 8 个设备像素下 440×956 点的视图,即 165×358.5 世界像素(PlazaRig.fit,第 433 到 456 行);图块中心和一个点下方的图块(TileMap.swift第 234 到 241 行);一次点击的世界点(PlazaRig.worldPoint,第 459 到 463 行);可行走字母(TileMap.swift第 35 到 53 行),应用从导出数据的行中读取它们(WorldMap.swift第 99 到 110 行);行走者的脚(PlazaRig.place,第 1088 到 1097 行,图表的 32×40 格子和脚下 2 行来自characters.json);以及放在玩家身上并限制在地图内的镜头(PlazaRig.lockCamera,第 1171 到 1200 行,配合Motion.swift中的CameraLock.bounds,第 258 到 262 行,它把边界向内取整到整像素;重放找到的两个停靠点都不在边界上,所以取整不改变任何东西)。脚本的 docstring 后来按这些行号做了更新,早先那次读取的数字保存在旁边的副本里;它的输出没有变化。它发现testTheChimneysSmoke的点击落在 (18, 8),即导出数据中的道具,所以收藏者停在 (19, 13),镜头在 (312, 278);从那里,house-dawn的阴影带(第 18 列,第 16 到 20 行)和house-moss第 3、4 行的阴影带都在视图内,至少留有 25.2 像素的余量,它的第 5 行(一个道具)也在;在横向 0.02 到 0.98、纵向 0.05 到 0.95 的归一化点网格上,没有任何点击能把house-mine的阴影带纳入视图;在玩家一楼的房间里,从它的到达点 (7, 10) 出发,视图横向覆盖图块 2.34 到 12.66,纵向覆盖整个房间,(3, 1) 的带窗帘窗户完整在视图内,而 (12, 1) 的窗户不在。 ↩↩↩ -
作者测量,2026年10月5日,在 Mac 上:
measure_palette_match.py,位于作者为本文准备的证据文件夹,输出palette_match.txt。它读取 pretpokecrystal在5beda23时gfx/tilesets/bg_tiles.pal(168 种颜色,57 种不同)、gfx/overworld/npc_sprites.pal(128 种,23 种不同)和gfx/tilesets/roofs.pal(108 种,91 种不同)中的每种颜色,把每个 5 位通道乘以 1/31,并与 KiradexDaylight.swift的 8 个颜色常量(按文本读取,第 28 到 31 行和第 77 到 80 行)比较:有两个精确匹配,都是纯白 (31, 31, 31),位于第 29 行和第 78 行;没有其他匹配;除此之外最接近的一对,第 79 行的 (1, 0.93, 0.84) 与 (30, 28, 26),在一个通道上相差 0.0323,即一个 5 位级。 ↩↩↩ -
作者重放,2026年10月5日:
replay_gen3_rain_thunder.py,位于作者为本文准备的证据文件夹,输出gen3_replay.txt。它首先对照 pretpokeemerald在731ad5b时的文本,断言它所依赖的写法:Rain_Main的三个状态,CreateRainSprite每次调用创建一个精灵并在创建第 24 个的那次调用返回 false,UpdateVisibleRainSprites的++rainSpriteVisibleCounter > rainSpriteVisibleDelay,Rain_Finish在下一天气为雨类时保留精灵,Sunny_Finish立即返回 false,Task_WeatherMain的切换(旧天气的finish()直到返回 false,然后initVars()、步骤计数器归零、调色板状态设为变化中,在src/field_weather.c中)以及UpdateWeatherColorMap的++colorMapStepCounter >= colorMapStepDelay,雷暴的设置状态和THUNDER_STATE_WAIT_CHANGE,以及Thunderstorm_Main周期的每个计时器和转换;然后逐次调用地重放它们。各部分:A,游戏中途从没有雨滴精灵的状态进入一种雨,从Task_WeatherMain切换天气的那一帧算起(第 1 次调用载入图集,第 2 到 25 次调用创建 24 个精灵,第一滴雨对雨在第 34 次调用、对更重的两种在第 30 次调用激活,最后一滴在第 115、105 或 145 次调用;调色板在第 59 帧到达映射 3;雷暴的第一道闪电在第 109 帧;载入地图时Rain_InitAll循环的调用次数,116、106 和 146);B,从一种雨进入另一种雨,精灵保留,数量每 5 或 9 次调用变化一滴;C,从THUNDER_STATE_NEW_CYCLE开始的雷暴周期,分别取最小抽值、最大抽值以及只有一道短闪电的情况(周期调用后 361 到 720 帧第一次落雷;闪电亮 6 到 8 帧;暗间隔 61 到 76 帧,即间隔计时器加上下一个状态所需的一次调用;长闪电满亮 30 到 45 帧;雷声在落雷后 1 到 20 或 1 到 100 帧)。 ↩↩↩↩ -
作者测量,2026年10月5日,在 Mac 上:
measure_collector_coverage.py,位于作者为本文准备的证据文件夹,输出collector_coverage.txt,读取提交66c116b0时 18:07 副本的人物图表和characters.json,并按读到的样子遵循CollectorComposer。A,面朝下第 0 帧的 32×40 格子,1,280 个像素:只算身体图表有 377 个不透明,123 张图表叠在一起有 581 个(把橡皮擦像素算作不透明则为 705 个);B,为站立的收藏者显示的 11 幅画面(PlazaRig.animate(_:))各自合成的清单默认人物:第一幅画面 408 个不透明像素,各画面之间 374 到 408 个,11 幅中都不透明的为 290 个;C,按存储颜色值逐通道的夜昼之比:默认人物的像素在提亮光下为 0.787(在那 290 个上为 0.788),在完整夜晚色调下为 0.573(0.575),提亮后最小的通道为 0.75;按图集平均颜色的地面,0.588;包含人物 408 个像素和 872 个地面像素的整个帧,正确的构建为 0.638,给行走者完整加色调的构建为 0.584,按身体覆盖为 0.633,按所有图表覆盖为 0.662。D 和 E 部分于 2026 年 10 月 15 日加入,A 到 C 保持不变。D,那 290 个像素的遮罩按各画面自己的颜色计算:白天亮度从 0.2989(doze_b)到 0.3161(think);两张截图为同一幅画面时,提亮下为 0.7875 到 0.7884,完整色调下为 0.5750 到 0.5768;在 121 个有序画面对上,提亮下从 0.7456(白天think、夜里doze_b,唯一低于 0.75 的一对)到 0.8336,完整色调下最高 0.6099;以idle_a保持时为 0.7876 和 0.5752。E,小镇图集所有不透明像素上的地面(提交cf3beb1e时 16:51 的副本),以及以idle_a保持的遮罩,在三种管线下:按存储值相乘,地面 0.5880,各通道 (0.500, 0.580, 0.900),遮罩在提亮下 0.7876,在完整色调下 0.5752;图片和色调从 sRGB 解码、相乘再编码,0.5623、(0.469, 0.554, 0.892)、0.7643 和 0.5312;图片解码而色调的数字当作线性值,0.7742、(0.719, 0.772, 0.951)、0.8885 和 0.7557;以及窗户参照,即capture_walk.txt中 Pro Max 到达视图内完整可见的 24 个发光格中的 7 个,294 个发光像素,全都在 24 个光池的平面(Air.lightPools)之外,并在距视图中心 450 点以内。F 部分,同日加入:空气的深度,云 2.4、漂浮物 2.45、光池 2.5,而行走者为 1 到 1.5(TileMap.depth);云最深的阴影,1 减去其最大 alpha(255 中的 110)乘以不透明度 0.55,得 0.7627;一个给行走者加色调的构建,在白天遮罩上叠加这层阴影后,按存储值为 0.7541,线性为 0.6964,色调数字当作线性时为 0.9908;在到达点 (19, 13),收藏者的帧在世界像素中为 x 296 到 328、y 258 到 298,不与 24 个光池平面中的任何一个重叠,离最近的一个 52 像素;以及当云的阴影在编码前于线性光中施加时,线性为 0.6076,色调数字当作线性时为 0.8645。G 部分,同日加入:管线色板,两种存储值为 51 和 102 的灰色,施加固定色调 0.25,三种管线分别给出 13 和 26、6 和 20、22 和 51,任意两种的差距都不小于 6 级;以及一个材质把每个色调再提亮一半的构建,它的色板读数为 32 和 64,距最近的一种 13 级,它的夜晚按存储值为地面 0.794024,各通道 (0.750, 0.790, 0.950),遮罩 0.893804。H 部分,此后加入:色板的位置,在 Pro Max 视图的点坐标中为 x 24 到 56、y 462 到 478,最远的角距中心 196.7 点;在到达时,以视图点坐标计,收藏者的帧为 x 177.3 到 262.7、y 424.7 到 531.3,不与色板重叠,7 扇参照窗户离色板 266.7 点或更远,检查 8 的 7 条阴影带离色板 144.0 点或更远。这是根据已发布图片的计算,不是截图。 ↩↩↩↩↩↩↩↩↩↩↩↩↩ -
pret,
pokecrystal/constants/ram_constants.asm,5beda23,2026年10月5日阅读:wTimeOfDay及其取值 MORN、DAY、NITE 和 DARKNESS(第 142 到 147 行),https://github.com/pret/pokecrystal/blob/master/constants/ram_constants.asm ↩ -
Blake Crosley,“Pixel-Art Worlds on iPhone: What the 16-Bit Masters Knew”,blakecrosley.com,2026年10月3日:第 2 世代那一段(“time of day in Johto is a 64-byte palette reload”(城都的昼夜是一次 64 字节的调色板重载);八个具名调色板)、光照分级表(Moonlighter、Celeste、Eastward、HD-2D;HD-2D“costs more than you’d think”(成本比你想象的高);Octopath Traveler II 的玩家会关掉景深)、RealityKit 做法,以及清单中的“Dusk and night as one multiply tint over the world plus hand-drawn emissive variants for lamps, windows and the kiosk sign”(黄昏和夜晚是覆盖整个世界的一次乘法色调,再为路灯、窗户和售货亭招牌加上手绘的自发光变体),https://blakecrosley.com/blog/pixel-art-world-on-iphone ↩↩↩↩↩↩↩
-
pret,
pokecrystal/data/maps/environment_colors.asm,5beda23,2026年10月5日阅读:EnvironmentColorsPointers(第 1 到 12 行:TOWN 和 ROUTE 指向.OutdoorColors,INDOOR 和 GATE 指向.IndoorColors,CAVE 和 DUNGEON 指向.DungeonColors,ENVIRONMENT_5 指向.Env5Colors),每张表每个时段一行八个调色板索引;.OutdoorColors(15 到 19),其 WATER 索引在早晨和白天为$28、夜晚为$29,即文件中的室外水面那一对,其他七个是区块自己的;.IndoorColors(21 到 25),早晨和白天为室内区块$20到$26加 TEXT$07,之后是夜晚和黑暗区块;.DungeonColors和.Env5Colors(27 到 37),区块原样,https://github.com/pret/pokecrystal/blob/master/data/maps/environment_colors.asm ↩↩ -
pret,
pokecrystal/engine/gfx/color.asm,5beda23,2026年10月5日阅读:LoadMapPals(从第 1197 行起:先在第 1198 行farcall LoadSpecialMapPalette,第 1199 行jr c, .got_pals,当该例程设置了进位时跳过背景复制;否则复制八个 8 字节的背景调色板;从第 1257 行的.got_pals起,复制来自第 1261 行MapObjectPals的八个对象调色板;然后在第 1269 行cp TOWN之后,在 TOWN 或 ROUTE 中复制来自第 1280 行RoofPals的地图组屋顶色,对 NITE 和 DARKNESS 通过第 1284 行的cp NITE_F取夜间那一对,在第 1290 到 1291 行把 4 字节写入wBGPals1 palette PAL_BG_ROOF color 1,即 ROOF 的颜色 1 和 2;八个背景索引来自第 1206 行的EnvironmentColorsPointers,按环境和时段),以及pokecrystal/engine/tilesets/tileset_palettes.asm中的LoadSpecialMapPalette(第 1 到 53 行:为TILESET_POKECOM_CENTER、TILESET_BATTLE_TOWER_INSIDE、环境不是 INDOOR 时的TILESET_ICE_PATH(殿堂)、TILESET_HOUSE、TILESET_RADIO_TOWER和TILESET_MANSION载入自己的表后设置进位;第 51 行的.do_nothing清除它),https://github.com/pret/pokecrystal/blob/master/engine/gfx/color.asm ↩↩↩↩↩↩ -
pret,
pokecrystal/gfx/tilesets/johto_palette_map.asm,5beda23,2026年10月5日阅读:城都图块集 192 个图块各自的调色板,https://github.com/pret/pokecrystal/blob/master/gfx/tilesets/johto_palette_map.asm ↩ -
pret,
pokecrystal/gfx/tilesets/roofs.pal,5beda23,2026年10月5日阅读:27 个条目,每个地图组一对早晨和白天的颜色、一对夜晚的颜色,https://github.com/pret/pokecrystal/blob/master/gfx/tilesets/roofs.pal ↩ -
pret,
pokecrystal/engine/overworld/events.asm,5beda23,2026年10月5日阅读:HandleMapTimeAndJoypad(第 193 行),当wMapEventStatus为MAPEVENTS_OFF时返回(第 194 到 196 行),否则调用UpdateTime、GetJoypad和TimeOfDayPals(第 198 到 200 行),https://github.com/pret/pokecrystal/blob/master/engine/overworld/events.asm ↩↩ -
pret,
pokecrystal/data/maps/setup_scripts.asm,5beda23,2026年10月5日阅读:十二个地图设置脚本(第 4 到 15 行的表);MapSetupScript_Fly(第 29 行,第 30 行mapsetup FadeOutToWhite,落入第 32 行的MapSetupScript_Warp,其mapsetup FadeInFromWhite在第 52 行,距结束两条命令);MapSetupScript_Door(102,第 103 行mapsetup FadeOutToWhite,落入第 104 行的MapSetupScript_Train,其mapsetup FadeInFromWhite在第 121 行);MapSetupScript_Connection(81)没有渐变,也没有脚本点名FadeInFromBlack或FadeOutToBlack。另有engine/overworld/events.asm第 1005 行,WarpToNewMapScript的newloadmap MAPSETUP_DOOR,即玩家踩上传送点时运行的设置;engine/events/overworld.asm第 619 行,newloadmap MAPSETUP_FLY;engine/overworld/scripting.asm第 2074 行,脚本命令warp用的MAPSETUP_WARP;home/map.asm中的FadeToMenu(1910,第 1914 行farcall FadeOutToWhite)、CloseSubmenu(1919)和ExitAllMenus(1927),它们调用ReloadTilesetAndPalettes(1966,其farcall UpdateTimeOfDayPal在第 1979 行),并以FinishExitMenu(1933,第 1938 行farcall FadeInFromWhite)结束;开始菜单engine/menus/start_menu.asm:StartMenu(第 13 行),自身没有Fade调用,在第 39、44 和 92 行call UpdateTimePals(home/time_palettes.asm第 20 行,UpdateTimePals调用_UpdateTimePals),它的.Exit(80),以及它call FadeToMenu的六个菜单项:StartMenu_Option(447)、StartMenu_Status(455)、StartMenu_Pokedex(466)、StartMenu_Pokegear(475)、StartMenu_Pack(482)和StartMenu_Pokemon(501),它们通过CloseSubmenu、ExitAllMenus或菜单的.Clear(第 155 行call FinishExitMenu)返回;FadeToMenu在mobile/和home/map.asm之外的 8 个文件中被调用 21 次,其中 6 次在开始菜单,其余在engine/events/specials.asm(8 次)、engine/pokemon/mail.asm(2 次),以及engine/items/mart.asm、engine/events/pokecenter_pc.asm、engine/overworld/select_menu.asm、engine/events/move_tutor.asm和engine/events/battle_tower/battle_tower.asm各一次,由gen2_daylight.txt第 E 部分统计;以及通过special调用黑色那一对的十一个地图脚本,每个渐出一次、渐入一次:正文点名的三个,maps/SproutTower3F.asm(第 55 和 59 行)、Route24(29、33)和TeamRocketBaseB2F(110、118),另外八个连同各自的两行列在gen2_daylight.txt第 E 部分,https://github.com/pret/pokecrystal/blob/master/data/maps/setup_scripts.asm ↩↩ -
pret,
pokecrystal/data/maps/maps.asm,5beda23,2026年10月5日阅读:map宏(第 1 到 15 行,八个字段:地图、图块集、环境、地标、音乐、电话标志、调色板模式和钓鱼组)、26 个地图组的指针(17 到 46),以及 388 个地图头,每个都有环境和调色板模式,其中包括五层广播塔(95 到 99),其音乐字段是表达式RADIO_TOWER_MUSIC | MUSIC_GOLDENROD_CITY;在 27 张PALETTE_NITE地图中,IlexForest(第 130 行)和TohjoFalls(第 161 行)是 CAVE,BurnedTower1F是 DUNGEON(第 91 行),DragonShrine是 INDOOR(第 160 行),UndergroundPath是 GATE(第 164 行),https://github.com/pret/pokecrystal/blob/master/data/maps/maps.asm ↩↩ -
pret,
pokered/home/overworld.asm,d2704a6,2026年10月5日阅读:传送代码对ROCK_TUNNEL_1F的判断和ld [wMapPalOffset], a(第 497 到 500 行),https://github.com/pret/pokered/blob/master/home/overworld.asm ↩↩↩ -
pret,
pokered/home/fade.asm,d2704a6,2026年10月5日阅读:LoadGBPal从FadePal4减去偏移量后读取,以及FadePal2:: dc 3,3,3,2, 3,3,3,2, 3,3,2,0(第 66 行),https://github.com/pret/pokered/blob/master/home/fade.asm ↩ -
pret,
pokered,d2704a6,2026年10月5日阅读:对wMapPalOffset的每一次写入。home/overworld.asm第 500 行(6,从室外地图传送到ROCK_TUNNEL_1F时)、第 535 行(0,传送回室外时;室内分支,第 508 行的.indoorMaps,不写入它)和第 790 行(0,在HandleFlyWarpOrDungeonWarp中);engine/menus/start_sub_menus.asm第 187 行(0,闪光);engine/events/black_out.asm第 7 行(0);engine/battle/init_battle_variables.asm第 18 行(战斗用的 0),并从engine/battle/core.asm在第 6667 行压栈、第 6768 行出栈的值恢复;以及data/maps/objects/RockTunnelB1F.asm,它的四条warp_event行(15 到 18)全部通向ROCK_TUNNEL_1F,https://github.com/pret/pokered ↩ -
Blake Crosley,“A Pixel Town Alive When Nobody Is Online: NPCs and Ghosts”,blakecrosley.com,2026年10月5日:《绿宝石》的
UpdatePerDay及其调用的函数,其中包括UpdateWeatherPerDay;以及其第 7.1 节简报中的房间时钟:“Every room runs on one clock: the server’s time in UTC, read in the room’s own time zone, one IANA zone per room in the server’s configuration”(每个房间都按一个时钟运行:服务器的 UTC 时间,按房间自己的时区读取,服务器配置中每个房间一个 IANA 时区),在welcome和安静广场的响应中以{"zone": "<IANA name>", "clock": <server ms>}发送,“and while the plaza is open the app takes its day-part from them rather than from the phone, so everyone in a room sees the same townspeople under the same light”(广场打开期间,应用从它们而不是从手机取得时段,所以同一房间里的每个人都在同样的光下看到同样的镇民),以及“Outside the plaza the phone’s clock stays the collector’s”(在广场之外,手机的时钟仍是收藏者自己的),如今读取Calendar.current的Daylight.now,以及它的检查 (b),“a client test with the phone’s zone set to a different one from the room’s asserts that the plaza’s day-part follows the room’s clock”(一个把手机时区设成与房间不同的客户端测试,断言广场的时段跟随房间的时钟),https://blakecrosley.com/blog/pixel-art-town-alive-when-nobody-is-online ↩↩↩↩↩↩↩↩↩ -
pret,
pokeemerald/include/constants/weather.h,731ad5b,2026年10月5日阅读:从WEATHER_NONE(第 4 行)到WEATHER_ROUTE123_CYCLE(第 21 行)的 18 个WEATHER_*值,把 SNOW、FOG_DIAGONAL 和 UNDERWATER 标为未使用的注释,以及 SHADE 的“Original name was closer to WEATHER_CLOUDY/OVERCAST”(原名更接近 WEATHER_CLOUDY/OVERCAST),还有 13 个COORD_EVENT_WEATHER_*值,https://github.com/pret/pokeemerald/blob/master/include/constants/weather.h ↩ -
pret,
pokeemerald/src/trig.c,731ad5b,2026年10月5日阅读:gSineTable(第 5 行),https://github.com/pret/pokeemerald/blob/master/src/trig.c ↩ -
W3C Web Accessibility Initiative,“Understanding Success Criterion 2.3.1: Three Flashes or Below Threshold”,WCAG 2.2 Understanding Docs,2026年10月5日保存为作者为本文准备的证据文件夹中的
wcag-three-flashes.html和.txt(A 级;“Web pages do not contain anything that flashes more than three times in any one second period, or the flash is below the general flash and red flash thresholds”(网页不包含任何在一秒内闪烁超过三次的东西,或者闪光低于一般闪光和红色闪光阈值);“Flashing content can cause migraines, dizziness, nausea, and seizures”(闪烁的内容可能引起偏头痛、眩晕、恶心和癫痫发作)),https://www.w3.org/WAI/WCAG22/Understanding/three-flashes-or-below-threshold.html ↩↩↩↩↩↩↩ -
pret,
pokeemerald/data/maps/*/map.json,731ad5b,2026年10月5日由measure_gen3_weather.py读取:518 张地图,每个头的weather、map_type、requires_flash和coord_events,例如 https://github.com/pret/pokeemerald/blob/master/data/maps/Route119/map.json ↩ -
pret,
pokefirered/data/maps/*/map.json,037335f,2026年10月5日由measure_gen3_weather.py读取:425 张地图,https://github.com/pret/pokefirered/tree/master/data/maps ↩ -
pret,
pokeemerald/src/field_screen_effect.c,731ad5b,2026年10月5日阅读:sFlashLevelToRadius和gMaxFlashLevel(第 53 和 54 行),以及以DISPLAY_WIDTH / 2, DISPLAY_HEIGHT / 2为中心的闪光效果(第 980 行),https://github.com/pret/pokeemerald/blob/master/src/field_screen_effect.c ↩ -
pret,
pokeemerald/src/overworld.c,731ad5b,2026年10月5日阅读:SetDefaultFlashLevel(第 971 到 979 行:除非设置了gMapHeader.cave,否则为等级 0;有FLAG_SYS_USE_FLASH时为 1;否则为gMaxFlashLevel - 1),以及include/global.fieldmap.h第 180 行,头部的u8 cave,由tools/mapjson/mapjson.cpp第 164 行根据每张地图的requires_flash写入,https://github.com/pret/pokeemerald/blob/master/src/overworld.c ↩ -
pret,
pokeemerald/graphics/field_effects/pics/,731ad5b(shadow_small.png、shadow_medium.png、shadow_large.png、shadow_extra_large.png),2026年10月5日由measure_shadows.py读取;未复现,https://github.com/pret/pokeemerald/tree/master/graphics/field_effects/pics ↩ -
Blake Crosley,“Pixel-Art Ledges, Stairs and Bridges in a Tile World”,blakecrosley.com,2026年10月5日:其第 3 节“The shadow stays on the ground”(阴影留在地面上),《红》《水晶》《绿宝石》中跳跃时留在地面上的阴影;以及它对 Sabotage Studio 的 Thierry Boulanger 的引用,出自 Screen Rant 2023 年 8 月 29 日的访谈,关卡必须“to all read well with different levels of height, with the light and everything”(在不同高度上、连同光照和一切,都要读得清楚)(https://screenrant.com/sea-stars-interview-thierry-boulanger/),https://blakecrosley.com/blog/pixel-art-elevation-ledges-bridges ↩↩↩
-
Stardew Valley Wiki,“Modding:Maps”,2026年10月5日保存为作者为本文准备的证据文件夹中的
sdv-modding-maps.html和.txt(光照属性AmbientLight、带九种光源类型的Light、WindowLight;以及NightTiles,“Changes the tile after 7pm”(晚上 7 点后更换图块),“mostly used for lamps in the game”(在游戏中主要用于灯)),https://stardewvalleywiki.com/Modding:Maps ↩↩↩↩↩↩ -
80.lv,“Moonlighter: Building Pixel Art & Preparing for Switch”,对 Digital Sun 的访谈,2026年10月5日保存为作者为本文准备的证据文件夹中的
moonlighter-80lv.html和.txt,https://80.lv/articles/moonlighter-building-pixel-art-preparing-for-switch ↩ -
Noel Berry,“Remaking Celeste’s Lighting”,2017年9月25日,2026年10月5日保存为作者为本文准备的证据文件夹中的
celeste-lighting.html和.txt,https://noelberry.ca/posts/celeste_lighting/ ↩↩ -
Game Developer,“Eastward’s creators share insights on making pixel art adventures”,对 Pixpil 的 Tommo Zhou 的访谈,2021年10月11日,2026年10月5日保存为作者为本文准备的证据文件夹中的
eastward-gamedev.html和.txt,https://www.gamedeveloper.com/art/eastward-s-creators-share-insights-on-making-pixel-art-adventures ↩↩ -
Nintendo Everything,Nintendo Dream 对 Octopath Traveler 开发者的访谈译文,2018年11月3日,2026年10月5日保存为作者为本文准备的证据文件夹中的
octopath-ne.html和.txt(Shizuka Morimoto 和 Tomoya Asano 谈光照),https://nintendoeverything.com/octopath-traveler-devs-on-character-origins-visual-style-initial-hd-rumble-plans-much-more/ 虚幻引擎关于 Octopath Traveler 的两个页面对抓取工具和 curl 都返回 HTTP 403(octopath-ue.html就是那个 403 页面)。 ↩↩ -
Wikipedia,“HD-2D”,2026年10月5日保存为作者为本文准备的证据文件夹中的
hd2d-wiki.html和.txt;二手资料(“Typically, a point light source is placed into the scene to make characters and objects cast shadows”(通常会在场景中放置一个点光源,让角色和物体投下阴影)),https://en.wikipedia.org/wiki/HD-2D ↩ -
Alana Hagues,“Triangle Strategy Producers Talk HD-2D And Why Other Devs Haven’t Used it”,Nintendo Life,2022年5月30日,报道 Tomoya Asano 和 Yasuaki Arai 接受 4Gamer 的访谈,导语为“It costs more than you’d think”(成本比你想象的高),2026年10月5日保存为作者为本文准备的证据文件夹中的
triangle-nlife.html和.txt,https://www.nintendolife.com/news/2022/05/triangle-strategy-producers-talk-hd-2d-and-why-other-devs-havent-used-it ↩↩ -
Sabotage Studio,Sea of Stars 媒体资料,2026年10月5日保存为作者为本文准备的证据文件夹中的
sos-presskit.html和.txt,https://sabotagestudio.com/presskits/sea-of-stars/ ↩ -
TechRaptor,“Sea of Stars Gameplay Clip Shows Day & Night Cycle”,2026年10月5日保存为作者为本文准备的证据文件夹中的
sos-techraptor.html和.txt,https://techraptor.net/gaming/news/sea-of-stars-gameplay-clip-shows-day-night-cycle ↩ -
Raymond Schlitter,“Pixelblog 21: Top Down Objects”,SLYNYRD,2026年10月5日保存为作者为本文准备的证据文件夹中的
slynyrd-objects.html和.txt,https://www.slynyrd.com/blog/2019/9/18/pixelblog-21-top-down-objects ↩↩↩ -
Raymond Schlitter,“Pixelblog 43: Top Down Tiles Part 2”,SLYNYRD,2026年10月5日保存为作者为本文准备的证据文件夹中的
slynyrd-tiles2.html和.txt,https://www.slynyrd.com/blog/2023/3/26/pixelblog-43-top-down-tiles-part-2 ↩↩↩↩↩↩↩↩ -
Raymond Schlitter,“Pixelblog 44: Top Down Trees”,SLYNYRD,2026年10月5日保存为作者为本文准备的证据文件夹中的
slynyrd-trees.html和.txt,https://www.slynyrd.com/blog/2023/5/22/pixelblog-44-top-down-trees ↩ -
Shmuplations,关于《圣剑传说 3》的石井浩一访谈译文,2026年10月5日保存为作者为本文准备的证据文件夹中的
sd3-shmuplations.html和.txt,https://shmuplations.com/seikendensetsu3/ ↩↩↩↩ -
Kiradex 源码,
Kiradex/World/Glow.metal,日期 2026 年 10 月 3 日,10 月 5 日 16:51 在提交cf3beb1e时、从当天正在编辑的工作树中读取(私有代码库):glowSurface,它把图片颜色乘以色调同时设为基础色和自发光色,不透明度为 1,位于注释“Light that adds, as light does”(像光那样做加法的光)及其那一行“black adds nothing, and a pool of light never darkens what it lies on, which an alpha halo did.”(黑色什么也不加,光池永远不会让它下面的东西变暗,而 alpha 光晕会。)之下(第 6 到 10 行)。 ↩↩↩↩↩↩ -
Kiradex 锻炉,
scripts/forge/air.py,日期 2026 年 10 月 3 日,10 月 5 日 16:51 在提交cf3beb1e时读取(私有代码库):docstring 中的图片,glow.png(“a radial pool, white at the centre falling to black”(一个径向光池,中心为白色,逐渐落到黑色))和cloud.png(“a tileable mask of cloud shadow: alpha where a cloud passes”(可平铺的云影遮罩:云经过之处有 alpha)),第 9 到 13 行。 ↩↩ -
Kiradex 测试,
KiradexUITests/KiradexUITests.swift,2026年10月5日 16:51 在提交cf3beb1e时、从当天正在编辑的工作树中读取(私有代码库):testThePlazaKeepsTime(第 502 行),以-plazaHour 21和-airProbe启动(503),以及testTheChimneysSmoke(523),以-plazaHour 12启动(524),等待空气的报告包含smoke 18(第 531 和 532 行),然后在归一化坐标 (0.38, 0.3) 点击广场(538),注释说这段路线“Up the grand stair onto the terrace, left of the fountain”(走上大台阶到平台上,在喷泉左边)(第 534 到 537 行);辅助函数wait(for:toRead:)(第 1171 行)、launch(第 1179 行,在app.launch()之前设置launchArguments)和save(第 1212 行,屏幕截图)。文件日期为 2026 年 10 月 5 日 10:32,行号以该副本为准。 ↩↩↩↩↩↩↩ -
Kiradex 源码,
Kiradex/World/HallView.swift,2026年10月5日 16:51 在提交cf3beb1e时、从当天正在编辑的工作树中读取(私有代码库;文件日期为当天 14:59):文件开头的注释“The show hall: off the plaza”(展会大厅:在广场之外)(第 5 行),以及大厅“is not networked”(不联网),进入时广场离开房间(9 到 11);“The show is entered through the veil.”(经由帷幕进入展会。)(34);以及它构建的PlazaStage(57),带client: nil(58)。到 18:07、提交为66c116b0时,文件字节相同。 ↩↩↩↩ -
Kiradex 设计计划,
docs/research/design/01-art-direction-v2.md,日期 2026 年 10 月 3 日,10 月 5 日 16:51 在提交cf3beb1e时读取(私有代码库):第 4 节“The plan, by build,”(按构建版本的计划),其中第 29 版构建列出了光池、云影、雾、落叶和萤火虫,以及“a vignette and tilt-shift depth of field as the camera’s rendering effects”(以暗角和移轴景深作为镜头的渲染效果)。 ↩↩ -
Apple Developer Documentation,
UnlitMaterial(RealityKit),页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的apple-unlitmaterial.html:“A material that doesn’t respond to lights in the scene”(不响应场景中光源的材质);iOS 13.0+,macOS 10.15+;以及其主题列表中的textureCoordinateTransform,“A two-dimensional transformation to apply to the entity’s primary texture coordinates”(应用于实体主纹理坐标的二维变换),没有说明偏移和缩放的顺序;还有init(applyPostProcessToneMap:),“Creates an UnlitMaterial with the given tone mapping setting”(用给定的色调映射设置创建一个 UnlitMaterial),没有说明普通初始化器在色调映射上做什么,https://developer.apple.com/documentation/realitykit/unlitmaterial ↩↩↩ -
Kiradex 源码,
Kiradex/World/SpriteSheet.swift,日期 2026 年 10 月 3 日,10 月 5 日 16:51 在提交cf3beb1e时读取(私有代码库;行号以该副本为准):帧材质的textureCoordinateTransform,偏移为(列 ÷ 列数,行 ÷ 行数),缩放为(1 ÷ 列数,1 ÷ 行数)(第 65 行),注释记录偏移是从纹理左上角量起的,“measured on the simulator”(在模拟器上测得);以semantic: .color创建的图表纹理(37);以及构建普通UnlitMaterial()的material(light:)(75 和 76)。另外Air.swift第 223 行的烟的帧也是这样设置的。 ↩↩ -
Kiradex 源码,
Kiradex/World/Motion.swift,2026年10月5日 17:18 在提交cf3beb1e时、从当天正在编辑的工作树中读取(私有代码库;文件日期为当天 12:36,到 18:07、提交为66c116b0时字节相同):它的头部注释,点明运动篇的简报是其来源(第 3 到 6 行);TickClock,不管显示器刷新率如何,都把整数个六十分之一秒交给世界,ticksPerSecond = 60(14 到 52,常量在 17);Stride.pixels,16(57),以及Stride.ticks(diagonal:running:)(67 到 74):直线行走一步 16 个刻度,跑步 8 个,斜向为 23 和 12;以及Stepping.tick()(215),当刻度数达到步幅的刻度数时结束一步。measure_kiradex_proposals.py和arith.py从这个副本读取这三个数字。 ↩↩↩↩ -
Kiradex 锻炉,
scripts/forge/town.py,日期 2026 年 10 月 3 日,10 月 5 日 16:51 在提交cf3beb1e时读取(私有代码库;行号以该副本为准):图例中的“a lamp post on paving (as l, with its shadow in the stone’s shade)”(铺石上的灯柱(同 l,它的阴影用石头的暗色))(第 29 行);在paint中(从第 403 行起),墙的阴影,“the top four rows of whatever stands at its foot”(墙脚下任何东西的最上面四行),调暗一档,“the way the light falls at Leene.”(就像 Leene 那里光落下的样子。)(第 426 到 436 行)。 ↩↩↩↩ -
Kiradex 源码,
Kiradex/World/FigurePreview.swift,日期 2026 年 10 月 4 日,10 月 5 日 16:51 在提交cf3beb1e时读取(私有代码库):第 25 行的@Environment(\.accessibilityReduceMotion),这是Kiradex/World/中唯一读取该设置的地方。 ↩ -
Kiradex 源码,
Kiradex/World/PlazaClient.swift,日期 2026 年 10 月 3 日,10 月 5 日 17:42 在提交5a344e3f时、从当天正在编辑的工作树中读取(私有代码库):handle(_:)的case "welcome":(第 214 行),它从服务器的welcome中取you、room、lines和players,别的什么都不取;endpoint(第 87 到 91 行),它从启动参数读取-plazaServer,按其注释的示例ws://127.0.0.1:8300/v1/plaza,“points a run at a local”(让一次运行指向本地的)服务器,否则使用生产环境的wss://端点;文件中任何地方都不读取zone或clock。18:19 在提交66c116b0时再次读取:字节相同。 ↩↩↩ -
Blake Crosley,“Pixel-Art Motion: Walks, Cameras and Doors on iPhone”,blakecrosley.com,2026年10月5日:简报第 1 项,“The walk is 1 pixel a tick, 16 ticks a tile; the run is 2 pixels a tick, 8 ticks”(步行每刻度 1 像素,每图块 16 刻度;跑步每刻度 2 像素,8 刻度),在 60 赫兹的刻度下,步行为每秒 60 像素;应用如今就在
Motion.swift中运行这个刻度和步幅(第 7 节),https://blakecrosley.com/blog/pixel-art-motion-on-iphone ↩↩ -
Apple Developer Documentation,
MaterialParameterTypes.BlendMode(RealityKit),页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的apple-mpt-blendmode.html:“Modes that describe how materials should be blended with content behind them”(描述材质应如何与其后方内容混合的模式);枚举值add和alpha;iOS 18.0+,iPadOS 18.0+,Mac Catalyst 18.0+,macOS 15.0+,tvOS 26.0+,visionOS 2.0+,https://developer.apple.com/documentation/realitykit/materialparametertypes/blendmode ↩↩↩↩↩↩ -
Apple Developer Documentation,
MaterialParameterTypes.BlendMode.add,页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的apple-blendmode-add.html:“Blend by adding the material’s color to the background color, ignoring alpha”(忽略 alpha,把材质颜色加到背景颜色上进行混合);“also known as additive blending or linear dodge”(也称为加法混合或线性减淡);可用性相同,https://developer.apple.com/documentation/realitykit/materialparametertypes/blendmode/add ↩↩ -
Apple Developer Documentation,
CustomMaterial(RealityKit),页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的apple-custommaterial.html:“A material that works with custom Metal shader functions”(配合自定义 Metal 着色器函数工作的材质);iOS 15.0+,iPadOS 15.0+,Mac Catalyst 15.0+,macOS 12.0+,tvOS 26.0+,https://developer.apple.com/documentation/realitykit/custommaterial ↩ -
Apple Developer Documentation,
TimelineView(SwiftUI),页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的apple-timelineview.html:“A view that updates according to a schedule that you provide”(按你提供的时间表更新的视图);iOS 15.0+,https://developer.apple.com/documentation/swiftui/timelineview ↩ -
Apple Developer Documentation,
TimelineSchedule.everyMinute(SwiftUI),页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的apple-everyminute.html:“A schedule for updating a timeline view at the start of every minute”(在每分钟开始时更新时间线视图的时间表);iOS 15.0+,https://developer.apple.com/documentation/swiftui/timelineschedule/everyminute ↩ -
Apple Developer Documentation,
UIApplication.significantTimeChangeNotification(UIKit),页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的apple-significanttime.html:在“when, for example, there’s a change to a new day (midnight), a carrier time update, or a change to, or from, daylight savings time”(例如进入新的一天(午夜)、运营商更新时间,或进入、退出夏令时)时发布;该页面列出 iOS、iPadOS、Mac Catalyst、tvOS 和 visionOS,没有标版本,https://developer.apple.com/documentation/uikit/uiapplication/significanttimechangenotification ↩↩↩↩↩↩ -
Apple Developer Documentation,
NSNotification.Name.NSSystemTimeZoneDidChange(Foundation),页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的apple-systemtimezonechange.html:“A notification posted when the time zone changes”(时区变化时发布的通知);iOS 2.0+,https://developer.apple.com/documentation/foundation/nsnotification/name-swift.struct/nssystemtimezonedidchange ↩↩↩ -
Apple Developer Documentation,
EnvironmentValues.timeZone(SwiftUI),页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的apple-timezone.html:“The current time zone that views should use when handling dates”(视图处理日期时应使用的当前时区);iOS 13.0+,https://developer.apple.com/documentation/swiftui/environmentvalues/timezone ↩ -
Apple Developer Documentation,
EnvironmentValues.accessibilityReduceMotion(SwiftUI),页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的apple-reducemotion.html:“Whether the system preference for Reduce Motion is enabled”(系统的“减弱动态效果”偏好是否开启);“If this property’s value is true, UI should avoid large animations, especially those that simulate the third dimension”(如果这个属性的值为 true,UI 应避免大幅动画,尤其是模拟三维的动画);声明为var accessibilityReduceMotion: Bool { get };iOS 13.0+,https://developer.apple.com/documentation/swiftui/environmentvalues/accessibilityreducemotion ↩↩↩↩ -
Apple Developer Documentation,WeatherKit,页面 JSON 于 2026 年 10 月 5 日保存为作者为本文准备的证据文件夹中的
apple-weatherkit.html:“Deliver weather conditions and alerts to your users”(向你的用户提供天气状况和预警);iOS 16.0+,https://developer.apple.com/documentation/weatherkit ↩↩↩ -
Kiradex 源码,
Kiradex/World/CollectorComposer.swift,2026年10月5日 18:07 在提交66c116b0时、从当天正在编辑的工作树中读取(私有代码库;文件日期为 10 月 4 日 07:40),连同Kiradex/Resources/World/characters.json(日期 10 月 4 日 07:20,与 16:51 那次读取时字节相同)以及同时复制的 123 张人物图表fig-*.png:头部注释,每张图表是“an 8-bit plane whose value is an index × 2 (0 clear, 63 the eraser”(一个 8 位平面,其值为索引 × 2(0 为透明,63 为橡皮擦))(第 5 到 14 行);sparse(_:),一个平面的不透明像素,即既不透明也不是橡皮擦的像素(66);layers(for:),总是加入身体、眼睛和手里的卡牌(131);以及indexPlane(for:),按清单顺序为每个朝向叠放各图层,最后一个不透明索引胜出(153)。 ↩↩ -
Kiradex 代码库,2026年10月15日 23:53 读取,未运行 git:把
.git/refs/heads/main当作文件读取,得到fc9d4876(10 月 15 日 17:45 提交),下面每个文件都用 zlib 从该提交的对象中取出,并与工作树中的副本逐字节比较,每一个都相同(私有代码库;行号以该提交为准):Kiradex/World/PlazaClient.swift,roomClock和roomZone(79 到 83)、roomDate,“The room’s time now, from the welcome’s clock”(房间此刻的时间,来自 welcome 的时钟)(109 到 113),以及读取json["clock"]和json["zone"]的welcome处理函数(265 到 267);server/app/main.py,携带"zone"和"clock"的welcome(21、22 和 228),以及ZONE,“one for the whole world until rooms are sharded by region”(在房间按地区分片之前,整个世界一个),未设置PLAZA_ZONE时为UTC(77 到 79);Kiradex/World/PlazaStage.swift,clock“re-read each minute: the light follows it”(每分钟重读一次:光跟随它)(64 和 65),读取它的relight()(86),以及“A quiet plaza joins no room”(安静广场不加入任何房间)(919);scripts/forge/protected_names.txt,“Names the world never uses”(世界永不使用的名字),以及server/tests/test_names.py,“The deny list: no franchise word and no canon place in anything the world names”(拒绝列表:世界命名的任何东西中都不得有系列词汇或经典作品中的地名)。在工作树的Kiradex/和KiradexTests/中搜索significantTimeChange和NSSystemTimeZoneDidChange,没有找到任何结果。读取脚本和副本都在作者为本文准备的证据文件夹中。 ↩↩↩ -
Kiradex 源码,2026年10月5日 16:51 在提交
cf3beb1e时、从当天正在编辑的工作树中读取(私有代码库):Kiradex/World/QuestBoardView.swift(日期 10 月 2 日),任务板,一个独立的视图,从PlazaStage.swift中的PlazaView(日期 17:22 的文件第 690 行,17:42 在提交5a344e3f时读取)和Kiradex/Views/Me/MeView.swift(日期 07:52 的文件第 65 行)以表单形式呈现。 ↩ -
Blake Crosley,“Pixel-Art People: Characters and a Creator on iPhone”,blakecrosley.com,2026年10月3日:在 iPhone 18 Pro Max 上,“a 440-by-956-point view at 3× shows 165 by 358 world pixels”(3 倍下 440×956 点的视图显示 165×358 个世界像素),https://blakecrosley.com/blog/pixel-art-characters-on-iphone ↩
-
Kiradex 源码,2026年10月5日 16:51 在提交
cf3beb1e时、从当天正在编辑的工作树中读取(私有代码库):Kiradex/Views/Me/MeView.swift(日期为当天 07:52),“我的”标签页的“Your Room”按钮me-room(第 272 到 277 行),它不需要广场的服务器就能打开玩家自己的房间;以及锻炉的scripts/forge/interiors.py(日期 10 月 3 日),它的ROOM网格(第 57 到 70 行)在一楼的 (3, 1) 放一扇带窗帘的窗户K,在 (12, 1) 放一扇窗户N,分别由window(第 259 行)和curtained_window(第 278 行)绘制。导出的room.json和room-up.json不带窗户格的列表。 ↩↩ -
Blake Crosley,“Pixel-Art Routes: Map Connections and Districts on iPhone”,blakecrosley.com,2026年10月5日:其简报中的名字与美术,“The deny list does not exist yet: the dossier promised one in the repository, and the repository has none”(拒绝列表尚不存在:调研资料承诺代码库中会有一份,而代码库里没有),并把建立这份列表及其测试列为该简报的一部分,https://blakecrosley.com/blog/pixel-art-world-beyond-the-plaza ↩