Eraphanis 更新日志
v0.1.9.3
新增:一键发布程序 tools/release.py,以及让 Windows 不再把产物当病毒拦下来的完整签名链路。
tools/release.py:用 Python 标准库重写整条发布流水线,替代三个 PowerShell 脚本。 七个环节(版本提升 / 构建 / 测试 / 签名 / 打包 / 校验和 / Git 发布)既能单独跑也能组合, 全部支持--dry-run——先把整条流水线的计划打印出来,再决定要不要真跑。 原来的三个.ps1保留为转发 shim,外部还在调用它们的流程不会断。- SmartScreen 的三处缺失一次性补齐:Release 链接加
-sstrip(去掉 2.88 MB overlay 和整套 DWARF 调试节)、补上 VERSIONINFO 资源块(resources/resources.rc.in,版本号由 CMake 注入, 永远与project(... VERSION ...)一致)、Authenticode 签名 + RFC 3161 时间戳。 产物从 9.14 MB 缩到 5.99 MB,overlay 归零、节区从 20 降到 11。 - 发行者署名
Causefficts Studios。证书是自签名的,所以同时导出根证书CauseffictsStudios-Root.cer随包分发——终端用户导入之后,SmartScreen 里显示的才是 正确的发行者,而不是「发布者未知」。 - 发布提交只收本次改动的文件:绝不
git add -A,仓库索引里经常压着别人的在途改动。 中文提交信息走 UTF-8 无 BOM 的临时文件(git commit -m在 Windows 上按控制台代码页编码, 中文会被弄坏)。推送逐个 ref 推,一次推多个会和服务端的 ref 锁竞争。 - 测试环节现在把
LOCALAPPDATA/TEMP重定向到构建树里的临时目录再跑。 之前测试直接往玩家真正的存档目录里写:一来会被机器上的行为防护当成可疑写入拦掉 (未签名的测试程序往用户目录里写文件,正好长成行为防护最喜欢的样子), 二来测试和玩家真正的存档互相踩。现在测试跑在自己的沙盒档案里, 本机全量 75 项自检 75/75 全绿(此前长期是 65/75)。 - 32 位版本现在真的能发布:补齐 mingw32 工具链支持,安装包按
Is64BitInstallMode自动分流——64 位系统装 x64、32 位系统装 x86。此前只有 x64, 安装包把同一份 64 位 payload 塞进两个槽位,32 位机器装上就是启动失败。 - 32 位构建改用 SSE 标量浮点(
-msse2 -mfpmath=sse)。MinGW 的 32 位默认走 x87 80 位扩展精度、只在存回内存时才舍入,而 64 位走 SSE2——同一份源码算出的结果末位不同, 于是世界生成的噪声确定性、武器姿态回落、UI 文字度量三项自检在 32 位上失败, 打印出来的数值还长得一模一样(actual=0.070000 expected=0.070000)。 两边统一舍入方式之后 32 位测试从未通过的若干项一次全绿。 - 音频依赖补上架构校验:mpg123 只发行
.def+.dll,而.def是架构中立的, 32 位构建能顺利链接 64 位的导入库、产出一个启动即被加载器拒绝的 exe。 现在 configure 阶段读 DLL 的 PE 头核对 machine id,不匹配就不启用 MP3 后端并给出警告; 打包阶段也会再核一遍 payload,混架构直接失败而不是放行。 - mpg123 与 OpenAL 的依赖关系拆开:缺 mpg123 只损失 MP3 播放(OpenAL、wav、ogg 照常), OpenAL 才是音频设备的必需品,它缺失才退回静音后端。此前缺任一者就整条音频链关掉。
v0.1.9.2
修复:两条长期失败的回归测试,以及一个空转的验收门禁。全量 72 项自检 72/72 全绿。
AudioParser:夜间 BGM 的 cue 指向audio/bgm/rainnight.wav,而实际发布的文件叫1rainnight.wav(带序号是为了播放列表排序),于是「夜间」这一段永远点不到真曲子。 cue 表改指真实文件。GroundFlicker:地面细节层一直锚在相机上而不是世界上——u_detail_origin这个 uniform 和Renderer2D::setDetailOrigin()从 v0.1.8.7 引入起就从来没有调用点,细节场跟着屏幕走, 相机一动地面就在它底下滑过去(这就是「跑起来地面闪烁」)。现在世界批锚到世界原点、 角色批锚到角色自身原点,平坦草场的 1px 相机位移残差从 0.00044 降到 0.000007 (步幅 0.00097 → 0.000053),与文档里记录的修复后基线一致。- 一键验收脚本
tools/building_acceptance.py的回归门禁此前是空转的:ctest 全绿时 打印100% tests passed out of N,而脚本只认X% tests passed, N tests failed out of M这一种格式,于是总数读到 0、报成「0/0 通过、无回归」——门禁恰好在全套绿的时候是瞎的。 现在两种摘要都能读,读不到摘要时直接判失败;失败豁免名单同步清空。
v0.1.9.1
新增:枪械与弹药的模型补全,物品栏图标改为模型缩图。
- 族内形状变体:武器表末尾追加第 39 列
model_variant——model_id选族(轮廓),本列选族里的形状。 柯尔特警用左轮 / S&W 警用左轮、温彻斯特 1897 / 雷明顿 10、春田 1903 / 莫辛-纳甘三对同族枪, 此前画的是逐字节相同的模型、背包里是同一张图;现在各自不同(短管 + 下方抛壳杆、木护木包住弹管 + 圆形弹仓盖、piled 前护木 + 下方弹匣 + 下挂枪机柄)。变体号必须落在该族已有的形状里,启动期校验拦下错号, 绘制层对不存在的变体回落到默认形状(画不崩,但启动校验会先报错)。 - 弹药模型:新增 4 个弹药族(手枪弹 / 12 号霰弹 / 步枪弹 / 特殊弹),与武器共用同一套画笔、同一张 材质遮罩(R 通道精确写材质 id 11..19,采样 NEAREST 且无 mip)与同一个图标拟合;弹药直立绘制,弹头朝上。
- 背包图标 = 模型缩图且字节同源:武器由
buildWeaponIcon(buildWeaponSprite(spec))派生,弹药由buildAmmunitionIcon(buildAmmunitionSprite(spec))派生。四种弹药不再是同一张哈希色「子弹盒」, 掉落物仍用模型、背包与快捷栏用图标,两者不混。 - 物品表末尾追加第 19/20 列
icon/model_id:弹药行写自己的模型 id,加内容是改表不是改代码。 - 表现层全部入口带变体:枪口锚点、抛壳口、持枪落位、地面拾取物与贴图缓存键都读
(model_id, model_variant, material_id)。修掉了抛壳口仍在用默认形状的缺陷——S&W 的弹壳 从枪身上不存在的洞里飞出来。 - 自检:新增
Reverseworks.AmmunitionVisual(弹药模型与图标,51 条断言);Reverseworks.WeaponVisual增加 W22 族内变体检查(1448 条断言);Reverseworks.WeaponE16E18Verify补齐 3 处变体盲点后 2399 条断言全绿。全量 72 项自检 70 通过,未通过的两项(AudioParser、GroundFlicker)是本版之前 就有的既有失败。
v0.1.9.0
新增:战斗系统三阶段落地(P1 动作框架 / P2 移动、格挡与远程 / P3 敌人、削韧、元素与词缀)。
- 动作表是数据:轻击三段链、蓄力重击三档、射击四式、翻滚与冲刺、五个技能、三个敌人招式; 起手/有效/收招三段状态机,带连招窗口、命中几何、部位映射与软锁。
- 翻滚的无敌从有效窗口第二帧才开始(贴边命中仍算),位移在世界里解算,撞墙停在墙边而不是穿过去。
- 格挡是姿态不是按键;完美格挡四帧窗口,反伤一半、硬直攻击者并送一次免费反击。
- 技能栏 4–9 六格(基础 + 四个核心 + 终极),冷却从技能“触发”起算;弹药是普通背包物品, 出膛即扣,空仓时枪变成棍子;瞄准辅助三档(关 / 12° 半程 / 6° 全程),只作用于远程。
- 敌人 husk / stalker / brute / spitter 跑的是玩家同一张动作表,招式读得懂;硬直等级 common / elite / boss 是数据(更大的韧池、更少的削韧、更重的伤害)。伤害类型物理/火/冰/毒, 抗性乘算、免疫等于伤害不存在;护甲只吃物理通道。
- 13 种装备词缀属于“物品实例”而不是定义,xorshift32 确定性 roll,随物品进背包、进存档; 存档格式升到 v8,旧档按“没有词缀的装备”读入。伤害、冷却、耐力共读 CombatStats 同一份汇总。
- 战斗 HUD:技能条 + 每格冷却遮罩 + 技能条下方耐力条。
新增:平台抽象层(PAL)下沉到引擎。窗口、输入、生命周期、显示枚举、资源源、路径、时钟、日志 移入 libs/uniscope-2d(include/uniscope/pal + src/pal/{glfw,android,common});游戏层 Application 只持有 uniscope::pal::IWindow,平台相关分支不再写进游戏头文件。Android 后端补上了 真实窗口/EGL 面,GlSurface 的空壳不再是唯一路径。
新增:音频接口化。IAudioDevice / IAudioStreamVoice 进入 include/uniscope/audio, OpenAL / WinMM / Null 后端移到 src/audio/{openal,winmm}/,由 AudioFactory 选择; OpenAL Soft 以随包动态库分发(LGPLv2 的“可替换”义务可满足)。Ogg 曲目现在能读出时长 (stb_vorbis 的 granule),剧情音乐时钟不再对 ogg 读到 0。
修复:主菜单的 BGM 播放器放不出 wav。播放器的 wav 分支只认 8/16 位 PCM,24 位母带被 直接拒绝(静默无声),而失败的曲目每帧重读一次整个文件,主菜单被拖住;列表也会永远 卡在这一首。现在 wav 由播放器自己解(8/16/24/32 位 PCM 与 IEEE float,单/双声道,任意 采样率),走与 mp3/ogg 同一条流式通道(可拖动进度、可循环、播完自动换下一首);放不 出的文件会被记住并跳过,列表继续往下走,不再每帧重试。
Android 返回:去掉内部手势里的"顶缘下拉",右缘内滑也一并去掉,只保留左缘内滑作为兜底 (系统返回手势可用时系统会先夺走触点并触发一次返回,内部手势只在 OEM 游戏模式吞掉系统手势时生效)。 同时复核了 Android 自带返回:AKEYCODE_BACK 经 PAL 映射为 ESC/Cancel 并由窗口消费, manifest 保持 enableOnBackInvokedCallback=false(legacy 输入路径)。真机实测: OPTIONS 与 KEYBOARD BIDDING 逐级返回、游戏内返回先存档再回主菜单,均正常。 新增(Android 触屏 GUI):菜单里的单指上下滑现在就是滚轮。手势先在 Application 层判别:轻点在 抬手时才补一次按下(按下沿不点击),所以从某一行开始滑不会顺手激活那一行;竖向滑动转成滚轮档, 横向滑动仍交给滑条/文本拖动,按住 0.55s 补一次按下。滚轮档按"手指按下的那一行"路由(不是当前 指尖所在行——一行只有约 3.3% 屏高,跟随指尖的话目标行早就滑走了):落在列表上滚列表/移动选择, 落在下拉值或滑条值上就滚那个值。OPTIONS 的 CONTROLS / GRAPHICS / AUDIO 页、KEYBOARD BIDDING 列表与主菜单都适用;桌面鼠标滚轮走同一条路径。自检 Reverseworks.TouchMenuScroll。 新增:触摸手柄自适应布局。所有控件以毫米为单位求解(手机 / 平板 / 窄窗口 × 左右手 × 用户缩放 0.7–1.5),两条不变量:全部在屏内、任意两控件不重叠;空间不足时退回紧凑布局。虚拟手柄没认领的 双指手势仍做相机缩放与“次要点按”(右键语义)。
修复:并行构建时每个目标都往同一个 bin/shaders 复制,copy_directory 会偶发 “No such file or directory” 并让构建失败;shader 部署改为进程锁串行(cmake/DeployShaders.cmake)。 修复:ctest 的工作目录统一到资源部署目录。此前 SaveSystem / SurvivalSystem / Character 从构建树 根启动,读不到 data/ 与 resources/,表现为“内容缺失”的假失败。 修复:AudioParser 引用的 audio/bgm/rainnight.mp3 早已不存在(实际发布的是 Rain Night.ogg); 命名 cue 表与自检改为指向真实文件,并补上 ogg 时长读取。
维护:清理仓库里的构建产物与排查材料(.vs、android/.cxx、android/app/build、out、tmp、 tmp_frames、旧 fork worktree 等,约 18 GB),.gitignore 补上这些路径;docs 下的两个旧 .docx 二进制文档删除,正文以 md 文档为准。
未完成(明确记录,不当作已完成):武器物品仍未进入 items.tsv,正常游玩拿不到武器;敌人只能由 测试或代码生成,世界里还没有刷怪;物品栏除 BAG 外仍是 NOT IMPLEMENTED YET; Reverseworks.GroundFlicker 仍是真实回归(地面细节在相机移动 1 像素时变化超限)。
版本:CMake / Android Gradle / 主菜单更新日志面板同步到 0.1.9.0(versionCode 9)。 v0.1.8.11
新增:OpeningPV 冷色雾场、缓慢漂浮的尘粒与克制的电影黑边;字幕按自身时长淡入淡出,结尾仍以白光揭示世界。 新增:cinematic_backdrop 专用着色器(冷色渐变 + 漂移雾层 + 中心光晕 + 暗角)。开场不再借用主菜单背景——后者带透视网格与扫描线,读起来像把主菜单摆到旁白后面。 修复:新存档加载后不再先淡入游戏画面再切入 OpeningPV;加载黑场现在直接衔接开场。 修复:PV 启动时立即应用首拍事件,避免首帧淡出/相机/字幕状态尚未生效而闪现游戏画面。 修复:跳过或重启 PV 时清理残留白场与未播放音频 cue,避免影响下一段剧情。 其他:文本绘制支持独立透明度,透明度不参与富文本布局缓存键,字幕淡变不会切碎缓存。
v0.1.8.8
修复:立物(树、栅栏、建筑)的碰撞看起来比模型往北约 1 格。 新增:CharacterRig::kRigUnitTiles(1 骨骼单位 = 0.5 格),把骨骼单位与格之间的换算显式化。
零、病因:骨骼单位不是格,而一半的代码当它是
- 渲染端把角色画在「相机世界标度(按 0.5 格取)× kRigVisualScale」上,也就是 1 骨骼单位 = 0.5 格;
kRigVisualScale的注释自己写着「骨架 1.0x 时约 0.54 格」。 - 但
drawnOriginLift/originLift/cameraAnchor以及全部探针都按「1 骨骼单位 = 1 格」算, 每一段骨骼长度都被多算了一倍。 - 于是
drawnOriginLift把角色整体抬高了0.5 × footOffset × kRigVisualScale ≈ 0.85 格: 视觉脚底落在pos.y + 0.52,而碰撞盒中心在pos.y - 0.33。 - 玩家向北撞障碍时脚先越过障碍、碰撞盒还在后面,于是每一件立物看起来都像 「碰撞在美术位置以北一格」(用户实测标注:栅栏 1.00 / 建筑 1.07 / 树 1.15~1.17 格)。
- 立物本身没错:同一帧实测,树 / 栅栏 / 建筑的碰撞盒南缘都正好压在自己的美术底边上(0 px)。
一、修法
CharacterRig::kRigUnitTiles = 0.5f:骨骼单位 → 格的换算系数,与渲染端共用同一个数。drawnOriginLift()=footOffset × kRigUnitTiles × visualScale − kSolesBelowCentre。originLift_=footOffset × kRigUnitTiles − kSolesBelowCentre,groundContactOffset()同源换算。cameraAnchor()=height × kRigUnitTiles × kRigVisualScale × 0.5 − kSolesBelowCentre(1.77 → 0.72 格):相机重新对准绘制图形的真实中点,角色不再偏在画面下方。- 渲染器里重复的
kRigVisualScale = 4.5f改为直接引用graphics::kRigVisualScale,unitScale的「0.5 格」改用kRigUnitTiles:两边不会再各写各的。
二、探针(以前它们和渲染器一起错,所以永远绿)
CollisionOffsetProbe/EntityVisualProbe改成复算渲染器同一套公式(含 0.5 系数)。 旧写法originLift − foot × scale会把foot × scale抵消掉:无论渲染器错成什么样, 它都报「视觉脚底 = pos.y − 0.33」。现在渲染器少乘一次因子,探针就会红。CharacterChecks补一条绝对断言(脚底恒在−kSolesBelowCentre),相机锚点断言改为 0.72 格。ShadowProbeChecks/NightBrightnessChecks的采样点同样按新约定换算。 夜间亮度探针的躯干采样点原先落在头顶之上的空气里(碰巧读到亮草地而「通过」), 现按骨骼单位落在胸口。RenderSmokeTest的取样窗口原本锚在关卡瓦片 (32,32) 而不是玩家身上,整个窗口落在视口外, 角色渲染检查一直是「0 像素」的假失败;改为按玩家位置、与渲染器同一套换算取样。
三、验证
- 探针:CollisionOffset / CollisionVisualAlign / VisualGroundLine / EntityVisual / CharacterChecks / ShadowProbe / NightBrightness / RenderSmoke 全部通过。
- 玩家脚底回到
pos.y − 0.33,与碰撞盒中心重合;贴墙间隙仍是契约里的 0.06 格。
v0.1.8.7
修复:跑步时地面闪烁。新增:档案 Echo 右键编辑菜单(重命名 / 控制台开关 / 复制 / 删除),顺带把所有角色共用的跑步姿势重新定相位,让人物也投出完整的太阳影。
零、病因:世界细节贴图锚在相机上,不是锚在世界上
- 地面细节层(草叶纤维、泥土颗粒、雪粒……)是从精灵的裁剪空间位置采样的。 这么做本来是为了让相邻地砖共享同一片连续细节(否则无缝贴图仍会以 一格为周期重复,重复本身就读成网格)。
- 但裁剪空间位置是相对相机定义的:相机一动,细节场在屏幕上原地不动, 地面却从它底下滑过去——细节于是贴着地面往回爬。停下来时相机不动, 地面也不动,看起来一切正常;一跑起来,地面的微观纹理就在脚下沸腾。 这就是「跑步时地面闪烁」。
- 文档里一直写着「细节锚定在世界」,着色器注释也这么写,但代码从来没做到: 少了那个把裁剪位置换算成世界位置的偏移量。
一、修法
- 着色器新增 uniform u_detail_origin:采样前先减掉它。 传入「世界原点」的裁剪位置,细节场就成了世界坐标的函数,牢牢钉在地面上; 传入角色自身的原点,衣料细节就跟着角色走,不会从身上滑过去。 (Renderer2D::setDetailOrigin,默认 (0,0),即旧行为。)
- 世界批与角色批分别设各自的锚点:世界锚到世界原点,角色锚到角色原点。
二、怎么确认的
- 新增探针 Reverseworks-GroundFlicker:把同一个世界从相距整整一个屏幕 像素的两个相机位置各渲染一次,比较「移动一像素后应当只是整体平移」的 残差。稳定的地面这个残差应当为零。
- 平坦草场实测:修复前 0.000444(跑动步幅 0.000972),修复后 0.000006 (步幅 0.000051),与「完全关掉细节层」的基线一致——即地面不再随相机 移动而改变。
- 逐子像素相位扫描(10 个相位)确认这不是某一相位的偶发,而是整条滑动。
- 对照实验:阴影开关、mipmap 开关各测一遍,地面残差不变——这次不是阴影, 也不是程序化图集的 mip 链。
- 修复前后同一帧画面逐通道比对,最大差 2/255,肉眼无变化:改的只是相位, 不是外观。
三、顺带记录(本次未改,仅排查结论)
- 阴影贴图是半分辨率、每帧按屏幕空间几何重新光栅化的,投影边缘因此以 2 屏幕像素为步长爬行。量级远小于上面的细节滑动(地面均值贡献约 0.000001),但低太阳角下会看见。
- 地面图集的 8px 环形 gutter 不随 mip 缩小:mip 4 级以上(视野拉得很远 时)双线性采样会越过图集格子边界,掺进隔壁格子的颜色。
- 地面砖块的 uvOrientation 旋转 90° 会破坏无缝场:旋转后的内容与未旋转 的邻居在共享边上是两条不同的曲线,硬边。网格是静态的,不闪。
四、新增:档案 Echo 编辑菜单(本次新功能)
- 右键档案列表里的 Echo,打开的不再直接是删除确认,而是编辑菜单: 重命名 / 控制台 / 复制 / 删除 / 取消五项。删除仍在其中,仍走原先的 二次确认面板;键盘也有同路——选中行按左方向键等同右键。列表行右端 的提示词相应由 DELETE 改为 EDIT。
- 重命名就地完成:NAME 行变成文本框,键入、退格、光标都在面板内, 不为改一个名字再跳一块屏。名单输入期间整个列表冻结——输到一半 选区被方向键挪走、字段跟着列表跑掉,比不提供重命名更糟。第一个 键入的字符替换旧名(如同全选后输入),Enter 落名,Esc 弃改; 纯空白的名字拒收,匿名的 Echo 在列表里无法辨认。
- 「启用控制台」是存档级开发者开关:勾选状态写入存档 [meta] 段 consoleEnabled=1,读档时回落,会话内经 GameState::consoleEnabled() 可查,下次保存随档带走——它是 Echo 的属性,不是碰巧在跑的会话的 属性。控制台本体尚未实现,这一版先通标记与管线;档案列表的次行在 开关打开时会追加 CONSOLE 标记:只在自己被打开的菜单里可见的开关, 是没人记得住的开关。
- 复制 Echo:整档读写一轮,新 id、名字追加 COPY,并逐条目复制该档 的 world 数据包——对白覆盖与剧本按 Echo id 存放,不复制的话副本 会静默回退到随包模板,不再是那个档的拷贝。副本保留原档的 savedAtTicks:它是那一刻的存照,不是恰好从那里出发的新会话;列表 按 id 平局排序,副本落在原档紧邻处。
- 文本输入管线(重命名的地基):GameInput 新增 typedText(本帧键入 的 UTF-8 串)与 backspace 边沿。桌面走 GLFW 字符回调,拿到的是 键盘布局与输入法实际产出的字符而非键码猜字,汉字名也能输入; 退格按码点折断而非按字节,半个汉字不会残留在名字里。Android 侧 键码映射表扩到全部可打印 ASCII,Shift 状态进字符大小写。
- UiShotProbe 新增 edit 截图模式(plain / console / rename 三个变体), 编辑菜单三态可一键重拍。
- MainMenuChecks 扩展:改名、开关控制台、复制、经编辑菜单删除一档、 删除尾档后回退转场,全部键盘驱动走完真实流程;39 项测试全绿。
- 修一个本功能上线前查出的连带 bug:改名落名后按取消回到档案列表, 列表里显示的仍是旧名。查因在 CxArchivesBrowsePanel 刷新列表前的 变更比对只看 id——改名不改 id,面板便判定「列表没变」,行则一直是 编辑菜单打开前构建的那批;控制台标记同理刷不掉。比对扩到行上真正 会画的字段(名字、控制台标记、日期戳、季节)。回归断言放在 「改名 → 取消 → 看列表」,且先做红绿验证:把比对退回只看 id, 断言立刻变红;期间还发现断言若放在「复制」之后会打不中——复制的 下一次重建恰好把新名带进来,顺序本身就是这个 bug 的藏身处。
五、游戏内开发者控制台(壳落地)
- 闸门与开合:只有档案编辑菜单里勾了「控制台」的那一幕回声才有 它。会话中按 `(反引号,固定键 RW_KEY_GRAVE_ACCENT,不进复键 位表——开发工具不该被普通绑定占走,菜单层也永远不读它)开合。 旗关着时这个键就是死键,一格输入都不产生。开着时整帧吞输入, 世界随之暂停(与物品栏面板同一套 ConsumedAndResetInput 语义), Esc 关面板;退场守卫补了控制台名额——开壳状态下的 Esc 若漏进 DemoScene 的退出判断,会把存档直接交回主菜单。
- 壳:顶置亚克力面板盖在 HUD 与世界之上,横幅「ERAPHANIS DEVELOPER CONSOLE」;输入行提示符 + 正文 + 插入符(不闪:闪 灭的插入符读起来像空行);历史区从底往上铺,最新一行贴着输入行 ——回显行(> 开头)正文字色、UNKNOWN COMMAND 警示色、横幅 说明暗色,扫一眼分得清「我说的」和「它答的」。滚轮回看限幅到 历史顶,任何新行到达都把视口拉回底部。历史封顶 200 行,超出丢 最老的。
- 命令列表与语法刻意留空:任何一行提交后回显该行,然后答 UNKNOWN COMMAND。注册表、语法解析与补全之后在 dispatch() 处 长出来,壳不再改——这是留给下一版的接缝。开壳那个 ` 字符不进 输入行:平台字符回调在边沿触发的同一帧把它送到 typedText,壳 里直接滤掉。
- 文本沿档案改名那条线:typedText 按码点追加、backspace 按码点裁,CJK 不撕半个字形;单行 120 字节封顶。Android 侧打字 管线两端已通,但反引号没有实体键位对应,开关暂只有桌面端可触发。
- 测试:新增 ConsoleChecks——壳的收字(含反引号过滤)、退格按 码点、提交回显与 UNKNOWN COMMAND、空行不出话、滚轮限幅与回底、 环形封顶,加上 InGameUi 闸门四态(旗开开壳、无旗死键、双 ` 开 合、Esc 关)与开壳时整帧 Consumed;40 项测试全绿。UiShotProbe 新增 console 截图模式供视觉复核。
六、所有角色共用的跑步姿势重新定相位
- 先说清楚:通用剪辑只有一套,CharacterRig 的 run 剪辑按每个角色的体型 重建,所有人物共用同一关键帧。这次不是调幅度,而是把整个循环按真实 步态重新定相位。
- 骨盆弹跳的相位原来是反的:承重膝顶得最高、腾空剪刀位反而下沉--那是 走路式的颠。现改为:触地齐平、承重下压最低、蹬伸升起、腾空剪刀位最 高(采样落差 37.7 milli-units,与设计值 0.038 吻合)。
- 摆动腿重做:蹬离地面后大腿快速前驱,新增第六关键帧把后摆(-0.76)接上 前驱峰(+0.30)再到晚摆(+0.62)与下一次前迈(+0.86),恢复动作有了加速 和减速。小腿的脚跟在晚摆期高抬贴臀(-1.18,比蹬离瞬间还深 0.26 rad) 再在后段甩开,让前掌近乎伸直着地。
- 手臂仍是约直角的泵摆,但肘部会呼吸:world rad 1.12 到 1.64 之间开合 (行程约 29 度),不再是焊死的直角。身体前倾随蹬伸脉动(spine_low 在 -0.165 与 -0.235 之间),颈和头每一拍反向顶住视线。
- 近远侧仍差一档(amp 1.0/0.94):两侧完全相同读起来像机械,这一点与 走路一致。
- 确认:CharacterChecks 新增 testRunPoseDrives,三条断言钉住这次的三个 特性(腾空-承重落差 > 0.02、摆动期折叠至少比蹬离深 0.10 rad、肘部行程 > 15 度),红测验证过:断言反转立即失败,恢复后 40 项测试全绿。循环采 样用 setMoveSpeedBlend 把速率钉在 1.0,按剪辑时间逐步推进。顺带记下 一个安全比较的依据:addAbsoluteTrack 对静止的父角解析世界值,所以 local rotation 的跨时间差值与关键帧差值只差一个常数,可以放心比较。
- UiShotProbe 新增 rig 拍图模式:同一套剪辑对触地/承重/腾空/晚摆四个 相位并排渲染(bin/ui_rig_run.png)。两个坑记下来:躯干 quad 要一批 提交完再 flush,文字标签留在 flush 之后画,夹在批中间提交会把后面的 角色吞掉;t=0 且没有 update 过画出来的是绑定姿势,采样点取相位 +1% 之后。
七、人物太阳投影:完整剪影,旧接地椭圆移除,脚底硬片跟随支撑脚
- 人物阴影现在和建筑、树木共用同一套 worldShadowCasters / ShadowCaster2D: RigRenderer2D 按同一份 pose/skin 生成每个投影部位的凸四边形,统一走太阳方向、 影长、宽度和沿落点衰减。多肢体重叠由 shadow map 的 MAX 混合压住,不会叠成 黑团。头/发参与剪影,手/脚/braid 仍不参与。
- 旧方案是在人物 quads 之前、同一个精灵批里画一个 64x64 软接地椭圆 (RigRenderer2D::drawShadow、shadowStrength)。它和新的投影剪影会在脚下叠成 第二层暗斑,所以移除;drawShadow 本身保留给独立软贴花和 UiShotProbe 的 blobtest,游戏里不再调用。
- 锚点仍是脚底站立线:骨架根在玩法锚点上方 originOffset() 提起,脚底再从根 往下 footOffset()(rig 单位)悬挂,屏幕锚点 = worldSizeToViewport(originOffset) − footOffset × unitScale;故意不含跳跃 抬升,身体升空时锚点留在地上,投影随高度变长。rig 偏移必须乘 rig 绘制标度 (unitScale = 相机标度 × kRigVisualScale),只有 figure-anchor 的世界语义走 相机标度;混用会把锚点浮到小腿。
- 脚底硬接触片改为跟随支撑脚:它不再固定在骨盆正下方(originClipX / groundClipY),而是用 contactFrame 找到两只脚,按脚踝高度选出支撑脚,再取 它的鞋底中点;按鞋底最低点选会被摆动脚绷直的脚尖骗到,按 quad 中心选又会 被脚掌俯仰带偏。双脚齐平(站立/双支撑)时取两脚中点,迈步时移到支撑脚。 固定片在支撑脚离开骨盆下方时会读成影子偏移。contactRadius 控制硬片 大小,玩家路径用 shadowRadius() × unitScale × 0.30;硬片是八边形鞋底轮廓, 纵向 0.55 比例,不再是旧的菱形大斑。
- 投影的 fall 需要单位换算:SunPath 的 shadowLength 按世界高度计,rig 的屏幕 Y 是视口高度,所以 WorldRenderer 用 −lightViewport / viewportUnitY.y × shadowLength;漏掉这步,人物影会短到只剩 脚下一小撮。
- 确认:ShadowProbe 新增玩家晨/暮回归,脚底反太阳侧必须比受光侧暗,且方向在 早晚反转(晨 ratio 0.803、暮 0.890);CharacterChecks 的 testProjectedShadowFollowsTheSun 钉住几何、方向与沿落点衰减。 RenderSmokeTest 的旧接地椭圆像素检查随 blob 一起删除,避免继续断言一个不再 存在的图层;UiShotProbe 的 rig 拍图模式也不再画 blob,blobtest / blobtest0 保留为 drawShadow 的净室变换诊断。
八、人物硬接地片独立成通道:太阳 pass 之后单独画,正午仍在
- 旧路径把脚底硬片塞进 worldShadowCasters,和太阳剪影共用 shadowA_; 正午 SunShadow.visible=false / alpha=0 时,整片阴影(包括接地片)一起消失, 人物在正午会失去与地面的接触,读成漂浮。
- 现在硬片走独立的 ShadowCaster2D:WorldRenderer 只把脚底补丁交给 appendRigContactPatch(),不再放进太阳 caster 集;Renderer2D 新增 setContactShadowCasters / setContactShadowAlpha,在太阳 pass 之后、有太阳时 再等 blurShadowMap() 之后画一次。它复用同一张半分辨率 shadow map 和 MAX 混合,所以太阳影仍柔, 接地片边缘不被 9-tap 高斯糊掉;重叠处取较暗者,不会二次变黑。
- 接地片 alpha 独立于 SunShadow::alpha:0.35 × shadowPresence × (0.75 + 0.25 × sunWeight) × shadowScale,白天/正午可见,跳跃中按原有 shadowScale 变淡,黄昏随 presence 淡出,夜晚归零。尺寸缩到 shadowRadius() × unitScale × 0.30,八边形、纵向 0.55,接近一只靴底, 不再像第二层软斑。
- 正午 shadowEnabled_=false 时,renderShadowMapPass() 不再把整个世界精灵批 送进 shadow shader,也不跑 blurShadowMap();只清空 shadow map 后直接画接触片, 避免为一张小补丁付整帧阴影贴图的成本。
- 渲染器仍按帧缓存三角化:太阳 caster 与接触 caster 各有一组 VAO/VBO 和 cache key,避免接触 pass 在每次 flush 时把太阳多边形重新三角化; beginFrame / releaseDeviceResources 都清空指针、alpha 与三角化 cache key。
- 确认:ShadowProbe 新增正午接地探针(无太阳剪影,只测脚底硬片),实测 contact=0.3350 / open=0.5096,ratio=0.657;晨/暮剪影回归仍为 0.803 / 0.890。 CharacterChecks 的 testContactPatchFollowsPlantedFoot 改为直接调用 appendRigContactPatch,并钉住八边形、宽度与 0.55 纵向比例。
九、应用图标换成怀表
- resources/echo.png:512×512 RGBA,主菜单 Logo 与 GLFW 窗口图标;按怀表主体 裁切、居中到透明画布,并清掉边缘杂散链节。
- resources/echo.ico:16/20/24/32/40/48/64/128/256 共 9 个 32 位尺寸,Windows 资源编译时嵌入 Eraphanis.exe;Inno Setup 安装包继续引用同一文件。
- resources/echo.icns:16/32/64/128/256/512/1024 及 Retina 变体共 11 个 PNG 块, 供 macOS App Bundle 使用。
- 文件名与 CMake / 资源引用未改,现有加载路径保持兼容;已重建 Windows Release 并验证 exe 内嵌图标与各尺寸均能正常读取。
v0.1.8.5
I 键物品栏 + 千禧年亚克力 UI 统一 + 浮动 Tips。
零、浮动 Tips(键盘选中 / 鼠标悬浮)
- CxUI 新增「浮动 Tips」:气泡跟随鼠标悬浮或键盘选中的那一项,显示该组件 的文字说明。说明文字挂在 CxComponent 上(setTip),所以任何组件都能带, 而「谁来上报」由宿主决定:悬浮由指针命中的组件上报,键盘选中由拥有选中 权的容器(CxButtonGroup / CxMenuListGroup)上报。
- 悬浮与选中是两个独立目标,指针有话说时指针优先——鼠标扫过一个没有说明 的组件,不该把键盘选中的说明也一起盖掉。目标只属于它被上报的那一帧: 不再上报就说明目标走了,气泡随之消失,不会留下一个指着空处的泡。
- 延迟:悬浮 0.40s、键盘 0.22s。键盘选中是明确动作,所以更快,但也不是 立刻——否则方向键划过列表会频闪。
- 键盘目标只在选中项被移动过之后才跟随:菜单打开时选中项本来就在第一项, 否则一进菜单就收到一条没人问过的说明。
- 气泡画在最后(MainMenu::render / InGameUi::render 的末尾),浮在所有 东西之上;默认在目标上方,顶部没空间时翻到下方,并被夹在屏幕内不越界。 键盘提示的描边用主题主色,悬浮提示用中性色,一眼可分。
- 主菜单各项、存档列表、物品栏页签与物品格都接上了说明:物品格显示物品名, 页签显示该页的一句话说明,空格子不显示。
零点五、字形图集改为「按内容烘焙」- 之前 CJK 图集烤的是 U+4E00 起的前 1200 个码位——那是按码位顺序切的 一段猜测,跟实际用字无关。结果是游戏自带对话里的 128 个汉字只覆盖 24 个 (19%),其余 104 个全部渲染成空白:排版对、字没有。
- 现在图集按内容烘焙:加载对话包后扫描出实际用到的字符,只烤这些(外加 拉丁基础集与中文标点)。SdfFont::loadForCodepoints() 做这件事, SdfFontLoadReport 会明确报出「要了但没塞进图集」的字符——那些会渲染成 空白,所以必须说出来而不是默默吞掉。
- 两路来源:编译进代码的字符串(建筑目录的名称与备注、更新日志面板的默认 标题)与该存档的对话包。两边都没有的中文字符串会渲染成空白,所以新增 中文字符串要一并登记。
- 实测:内容共 460 个不同字符,旧切片只覆盖 21%;按内容烘焙后图集 786 个 字形、零缺失——比旧切片装得更少,却每一个都对。
- 加载流程因此改为等对话解析完再烘焙图集:先烤默认切片等于白白花掉一千次 字形烘焙,而这个存档根本用不到那些字。
零点六、剧情动画(PV)脚本层
- 新增 libs/pv:.pv 剧情动画脚本的解析器与时间线运行时。这层只吐抽象事件, 完全不认识相机、光照和渲染器——由宿主决定每个事件意味着什么,所以它可以 无图形环境单测,同一条时间线也能在游戏里演、在编辑器里预览。
- 单独一种格式而不是复用 .cdlg:对话是「选择的图」,PV 是「时间线」。cdlg 是个 干净独立的组件,电影级概念(时间、镜头、图层)不该进去。两种格式只在一处 交汇:dialogue <cdlg_id> 把控制权交给对话,PV 在它结束时接着走。
- 指令:shot(镜头,带秒数是推拉、不带是切)、follow_player、time、weather、 letterbox、fade、narrate、say、hold、advance、goto、if、set、clear、dialogue、 music/sound、end。一个 beat 里的指令按序执行,只有分支指令会离开直路; 一个 beat 跑完落到下一个,所以 beat 是有序的。
- 台词用引号包住:文字是散文,需要自己的分隔符,且闭合引号后还有内容算错 误而不是被悄悄截断的行。
- 事件驱动的状态机:Playing / Holding / AwaitingInput / InDialogue / Finished。 两条容易踩的规则:游标在指令「开始」时就前移(卡片、等待、对话都会阻塞时 刻),游标停在卡片上会导致它被关闭后又出现一次;悬浮与键盘是两个独立目标, 指针有话说时指针优先,所以鼠标扫过没有说明的组件不会盖掉键盘选中的说明。
- 找不到对话不会卡死剧情:skipDialogue() 让时间线继续走。
- 工具 pv-cli:--dump 打印解析出的时间线,--lint 报 beat/卡片/等待/分支数量与 计时长度。示例脚本见 data/Eraphanis/world/template/pv/openingintro.pv。
- 尚未接进游戏:M2 是绑定层(src/game/PvSystem 把事件变成镜头、时钟、遮幅与 文字卡,DemoScene 加一个 Mode),新建存档的开场 PV 挂在加载淡入结束处 (那时屏幕正好是黑的)。
零点七、剧情动画(PV)绑定层
- src/game/PvSystem 是时间线与游戏之间那一层:库只吐事件,这一层决定每个事件 意味着什么——shot 变成镜头推拉,time 变成时钟(光照本来就是从时钟推出来的, 所以改小时真的会改天色),letterbox/fade 变成遮幅与黑场,narrate/say 变成 文字卡,dialogue 变成交给 .cdlg 对话并等它结束,set/clear 变成时间线旗标。
- PV 运行期间世界是冻的:GameState::update 根本不跑,所以时钟不走、天气不演、 玩家不动;相机只因为时间线说了才动。
- 镜头另起了一种「按时长」的移动(Camera2D::beginScriptedMove):游戏里的跟随机 是按固定速率指数逼近玩家的,那套模型套不上镜头——镜头有明确的长度,下一段的 时间是照着这段写的,相机必须准时到。
- 文字卡是一块深色底板加一条主题主色的细线,不用菜单那套亚克力面板:亚克力 是渐变的,没有任何单一文字颜色能在它的每一段上都读得清,而字幕得压在任何一 帧画面上都读得清。
- 打字机按「码位」而不是「字节」推进:一个汉字是三个字节,按字节算只会露出三 分之一的句子。drawRichText 加了 revealChars 参数,切的是已经排好版的绘制, 所以每帧不用重新排版。
- PV 包跟对话包一个规矩:随包发布的脚本在存档首次加载时复制进存档自己的世界 目录,存档自带的优先于随包的;早于这个功能的存档仍然播随包的开场。
- PV 包在「加载时」解析,不是在开场动画开始时:字体烘焙等的是加载的 worker, 台词的字形不在图集里就是一片空白——collectPvTexts() 把 PV 的卡片字样送进 和对话同一趟烘焙。
- PvSystem 挂在 GameState 上,和它交接过去的 DialogueSystem 做邻居:加载阶段 解析好包再交过来(GameState::applyPvStaging),时间线驱动的是模型的相机、 时钟与对话。
- DemoScene 多了一个 Mode::PlayingPv:挂在加载淡入结束处(那时屏幕正好是黑 的),而且只给「全新存档」——读档的玩家已经看过开场了。PV 期间不画 HUD:剧情 动画不是暂停的游戏上面压一条字幕。ESC 可以跳过。
- 检查 tests/PvSystemChecks.cpp 直接跑随包的开场脚本,所以钉住的就是游戏真的 会演的那一段:相机动了、遮幅是黑的、文字卡升起并逐字显现、确认键推进到下一 句、脚本写的小时到了世界时钟上。它还会把一帧写成 pv_frame.ppm,方便肉眼 过一遍。
零点八、剧情动画(PV):音频提示与世界内的触发
- PV 的 music / sound 指令现在真的会响。原先它们只吐事件、宿主什么都不做——随包 开场脚本里那句 music theme_calm 一直是句空话。
- 时间线只说「该听见什么」,不碰音频引擎:PvSystem 把提示攒起来,宿主取走去播 (takeAudioCues),因为引擎是宿主的东西。
- 提示名的解析在 src/audio/AudioCueCatalog:音乐提示说的是「情绪」不是「文件」—— theme_calm 是主题曲,theme_night 是雨夜;表里没有的名字可以直接写音轨的文件名 (music rainnight),所以新音轨一发出来就能用。音效提示写的是效果名(sound destruction),扩展名由文件夹决定。
- 剧情动画的音乐是「换掉」世界的音乐而不是叠在上面(只有一条音乐通道),剧情结束 后世界的音乐回来(endStoryMusic)。
- 脚本现在可以声明「什么让它开始」: trigger walk <东西> <半径> 玩家走到离它多少格 trigger interact <东西> 玩家对它按交互键
- 触发写的是东西的「种类」而不是坐标:生成世界里单个实例的 id 带着坐标 (shrine_12_34),写不进脚本;种类写得进(shrine),而且指的就是那一个。 为此 Element 有了 typeId()。
- 一个场景只演一次。演过哪些场景存在存档里(SaveGameData::playedScenes),所以 剧情动画不会变成「每次读档都看一遍的过场」。老存档没有这一段就是「都没演过」, 不需要迁移。
- walk 触发在玩家跨进新的一格时才检查,不是每帧:找那个东西要翻世界的元素表, 那不是每帧该干的活。
- 一次交互只属于一样东西:现在由 GameState 居中处理交互事件——被场景认领的交互, 那件东西本来会开的对话就不会开(除非场景自己用 dialogue <id> 交接过去)。
- 随包示例:data/Eraphanis/world/template/pv/stovewarmth.pv——玩家走到炉子旁边, 场景开始。
零点九、对话渲染(对话终于看得见了)
- .cdlg 对话系统一直只有逻辑没有画面:说话人、台词、选项、旗标、信号、键盘选择全都 实现好了,游戏里却一个字也没画——随包那段石碑对话写得很好(带分支选项),却完全 看不见。这次给它配了对话框。
- 对话框在玩法视口底部:说话人、正文、下面是需要时给出的选项。底板是主题最暗的颜 色加一条主题主色的细线——不是菜单那套亚克力面板。亚克力是渐变的,没有任何单一 文字颜色能在它的每一段上都读得清,而对话得压在任何一帧画面上都读得清。
- 底板与文字的规格挪到 src/game/NarrationBox:剧情动画的字幕卡和对话框是同一种叙 事声音,共用同一块底板。
- 选项居中排在正文下面,选中的那条旁边有一根主色竖线。选项行就是那行字自己的框, 所以指针按到的是字,不是字周围的空地。
- 键盘与指针都能选。键盘本来就有(上下键 + 确认);指针是宿主的活:视图做命中测 试,宿主告诉对话系统选了哪一条——对话系统知道词,不知道词在屏幕上的哪儿。
- 对话框两种模式都画:玩法模式画在 HUD 上面,剧情动画模式画在剧情自己的图层上面 ——剧情动画会把控制权交给对话,而对话必须看得见。
- 说话时 HUD 收起快捷栏:对话框就占在快捷栏的位置,而别人在跟你说话的时候你不会 想伸手去拿快捷栏。
- 剧情动画交接给对话时,字幕卡会收起——卡片和对话是同一个声音在同时说两件事。
- 检查 tests/DialogueChecks.cpp 直接跑随包对话包:话在屏幕上、选项在能按的地方、 按下去就选中了;还会把一帧写成 dialogue_frame.ppm 方便肉眼过一遍。
一点零、开场 PV(黑屏、白字、光)
- 新建存档进入世界时播的第一段东西换成了「开场 Prologue」:纯黑底、白字旁白,播 完音乐之后一行字、延迟两秒一声怀表上弦的 ding,然后光从中心向四周把黑幕推开、 逐渐过曝、最后柔焦淡出,切进游戏画面。
- 旁白用的是新指令 caption <秒> "<文字>":它不等玩家按键,到点自己走。narrate 是「等玩家」的那种卡,caption 是影片里的一行字幕——影片不会等读者,而且字幕是 整句出现的,不会自己一个字一个字打出来。
- 音乐结束那一刻是真的「音乐结束」:新指令 wait_music 会把时间线停在那儿,直到 宿主说音乐放完了。等的对象是音乐本身而不是一个计时器,所以音轨被重新剪过、重新 混过,那一刻也会跟着动。库不认识音乐——是宿主盯着音频引擎,把它放行。
- 光是新指令 whiteout <秒>:从画面中心亮起,把黑暗向四周推开、过曝到纯白,再变薄 褪去,世界从光里透回来。它是一个着色器(shaders/whiteout.frag),并且这段时间 时间线是停着的——光没褪完,序章就没完。
- 纯黑屏上字幕是「标题卡」而不是「字幕条」:底板不画(背后没有东西要挡),文字 居中在屏幕中央而不是贴在底部。
- 屏幕全黑的时候世界根本不画:背后什么都没有,一段几十秒的黑屏不该白烧世界的渲染 性能。
- 随包脚本:data/Eraphanis/world/template/pv/openingprologue.pv。
- 之前设计的开场 PV(opening_intro)还在包里——它现在是一个自己的场景,不再是新存档 的第一件事;开场对话(石碑那段)原样保留,玩家还是走到石碑前跟它说话。
- 顺手修了一个 M2 留下的 bug:fade 的方向是反的(fade out 反而把画面放亮)。这次序章 第一个指令就是 fade out,一跑就撞上了。
- 主旋律换回四十八秒的版本,旁白各行按新长度重新铺(末行收在音乐将尽处,wait_music 自己等着音乐真正结束)。
- 修掉一个「开场 PV 不播、石碑对话也消失」真机 bug:存档自己的 pv 目录里只要有过一个 脚本,连同包 ensure 就提前收工——老构建给存档铺过的旧脚本(只有 opening_intro)会把 新的 opening_prologue 永远挡在门外,于是序章静默跳过;而第一段石碑对话本来就是 PV 里 dialogue 指令交接的,序章不播、对话跟着一起没。现在每次进世界都把随包脚本 merge 一遍(skip_existing,存档已有的一个不动),旧存档与新存档 alike 都能收到 完整的包;开场改为 startScene 起播,「声明即记录」让今后任何存档都不会重播。
- 再修一个「PV 有字幕没声音、字幕完直接黑屏卡死」的真机 bug:序章的音乐和世界 BGM 共用同一条音轨,而世界 BGM 的三条规则会顺手把它也管了——离开菜单时 menuBgmFadeGain_ 淡到 0(音乐被淡成静音)、syncBgmPlayback 因为「世界不想要音乐」把音乐暂停、 applyBusGains 每帧又把「单曲列表要循环」的规则按回去(音乐永远到不了结尾)。 于是序章听不见声音,wait_music 等一个永远不会结束的音乐,只能 ESC 退出。现在序章的 音乐归序章管:播放期间世界的淡入淡出、暂停/恢复/重播、循环规则一律不碰它,它按玩家 的音乐音量播一遍;新增 AudioEngine::storyMusicFinished()——「序章的音乐放完了吗」 由后端自己的报告或音轨自己量出来的长度回答,等待因此不会被任何后端或暂停卡死。 MusicPlaybackInfo 增加了 looping 字段,让「播一遍」这件事可被检查。
- 又修一个真会卡死的 bug:OpenAL 起不来而回落到 Winmm 的机器上,PlaySound 对播放进度 什么都不肯讲,wait_music 只能永远等下去,新存档进来就是一块永不结束的黑屏。现在 cue 起音乐时会把音轨自己的长度从文件字节里量出来(mp3 逐帧走、wav 走 RIFF——见 musicDurationSecondsFromPath),量不到报告的后端就按这个长度计时;音乐压根没起来的 话,等待也当场放行。减一分风险:一场黑屏的电影不该撞死在一台没有 OpenAL 的机器上。
一、I 键物品栏(全新)
- 面板是四段条带:页签 / 快捷栏行 / 物品网格 / 键位提示,全部共享同一套 「格边 e」刻度,面板宽高、格子尺寸、文字字号一律按 e 等比推导。
- 网格由 5 列改为 8 列,面板随之加宽、格子随之变小。列数就是「面板宽度 ÷ 格边」的比值:加一列 = 加宽一格 + 按面积预算把格边压小一档。16:9 下面板 从 662×688(玩法视口的一半宽)变成 902×495(近七成宽),物品格从 122px 缩到 107px,一行能看到的物品从 5 件变成 8 件。
- 宽度预算是硬的(面板必须留在玩法视口内),所以 8 列的网格把限幅口径从 「视口高度」换成了「屏幕面积」:格边跟着视口一起伸缩,宽屏家族不再共用 同一档,窄屏仍由宽度限幅牵制收格。
- 行数是硬需求而不是优化变量。旧算法在「屏幕面积 2/5」与「视口高 96%」 冲突时挑了面积占比更高的方案,于是基础 15 格只给 2 行,另外 5 格藏在 一个没有指示的滚动窗口里。现在容量决定行数,格边反过来由面积/宽/高三条 上限里最紧的一条解出;只有格边收到下限仍然装不下才退化成滚动窗口。 16:9 下 3 行整屏可见,物品不再需要靠滚轮翻。
- 面板在玩法视口内水平+垂直居中(原先锚在视口顶缘,和顶部 HUD 挤在一起)。
- 快捷栏 3 格与网格同刻度、正方,中心精确落在网格第 0/1/2 列上。原先 CxSlotStrip 的 setCellHalf() 从没被调用,用的是组件默认半边 0.05 画的 竖条,数量角标还按网格格边算字号,直接溢出到格子外面。
- 网格原点的横坐标补回半格:第一列格心 = 网格左缘 + 格边。原先写成左缘, 整块网格左移半格、凸出面板边框,右侧反而空出半格。
- 格子坐标只剩一份:游戏侧读数与 CxSlotGrid 组件的公式逐字对齐,并加逐格 一致性自检,杜绝「第二处手写公式」再次漂移。
- 溢出时网格右缘内侧出现细滚动条(轨道 + 滑块),槽位占用数写进快捷栏行 右端(页面标题 + 12/15)。
- 页签 8 个标签统一字号(取最长标签能塞下的那个),按栏宽 75% fit,不再 互相贴脸;键位提示两侧留半格,文字行盒真正垂直居中。
- 格子床面内缩 11%,15 个空格读起来是一格一格,而不是一整块黑板。
- 物品模型:Inventory 基类 + PlayerInventory(基础 15 格)与独立的 3 格 快捷栏;拖拽取放、同类合并、Ctrl/Shift 快速移动、装备槽与穿戴判定、 存档序列化。
- 修掉 MSVC 编译不过的问题:InventoryCxPanel.cpp 用了 C 风格复合字面量, MSVC C4576 直接报错(GCC 当扩展放行),这份代码在 MSVC 工具链下根本 构建不出来。
二、千禧年亚克力 UI 统一
- 在 complexui 里建了一套共享原语:落影 → 半透明渐变体(上缘乳白通透、 下缘沉底)→ 顶部高光带 → 一亮一暗双棱(上/左受光,下/右背光)→ 内缘辉光 → 细颗粒。面板、按钮、格子共用,保证主菜单与游戏内说的是 同一种视觉语言。
- drawCxPanel / drawCxGlassPanel / drawCxHeroPanel 走同一条路径,只差强度: 游戏内面板底板接近不透明(物品在花花世界背景上仍然读得清),主菜单铭牌 更通透(能看见背后的场景),覆盖层最透。
- 主菜单按钮:静息保持扁平(只有一道底边发丝线),悬浮浮起半透明亚克力 块(底下压一道落影),按下则整块内陷(双棱翻转、高光压到下缘、底色 变实)并甩出一圈向外扩散、逐渐淡出的高光环。
- 菜单行、选项页签、分隔线、滑杆、复选框、高亮文字底板统一到同一套 语汇:菜单行从整块实心荧光改成半透明亚克力块,文字改回浅色保证对比度。
- CxTheme 新增 8 个亚克力 token(面上缘/下缘/受光棱/背光棱/高光/柔光/ 辉光/投影),由 12 套主题种子各自烘焙——所有主题自动得到一套自洽的 亚克力配色,不必逐套手调。
- HUD 侧:物品格与快捷格改成内凹的亚克力井(有物品透一点主题色 / 悬停 透光 / 选中整块抬起),生命体力等条改成暗槽 + 槽底亮边。
- 顺手修掉一个真 bug:drawVerticalGradientBands 的条带步进用了半边高而 不是整条带高,渐变只铺满矩形上半截、下半截直接断色成一块平板(旧面板 下半截永远是平色就是这个原因)。
三、配套内容与工具
- 装备目录与穿戴槽、疾病系统(感染部位与病程)、门与室内旅行(走近开门 淡入到锚定空间,不再一帧瞬移)。
- 男女角色共用 HumanoidCharacter 骨架(体型/朝向/移动/体力/跳跃/动画), 体型可调。
- 物品图标改为程序化生成的小图集:形状由物品类型推导、色相由 id 哈希决定; 之后接手绘资源只需覆盖同名帧,调用方零改动。
- 音频:新增 UI 反馈录音(按钮按下/选中)与夜间雨声,AudioSamples 把录音 目录化(往 resources/audio/sound 里丢文件就是加一行表)。
- 城镇与地形:程序化建筑/地砖扩展,动态材质与阴影修正。
- 新增离屏 UI 截图工具 Reverseworks-UiShotProbe(menu/panel/buttons 三种 模式):风格改完一条命令就能复核,不用开游戏。
v0.1.8.2
渲染器状态查询按帧缓存:flush 的 CPU 开销降 16%。
精灵批处理 flush 原先是每次 flush 都问驱动要当前绑定的 framebuffer 和 viewport(glGetIntegerv ×2),因为阴影 pass 要绑定自己的 FBO、 算出后还得还原回原来的目标。每个 glGetIntegerv 都是一次驱动往返, 必须先把命令流排空才能回答,实测两项合计 0.44 ms;一个世界帧要 flush 14 次,等于每帧白烧 6 ms 在"渲染器本来就该知道的状态"上。
- 改为每帧只问一次:beginFrame / beginDynamicScene / endDynamicSceneAndPresent 三个入口处调 noteRenderTargetChanged() 作废缓存,flush 时若缓存有效就直接用。一帧之内目标恒定—— Renderer2D 是引擎里唯一绑定 framebuffer 的地方,它自己的每次绑定 都走 noteRenderTargetChanged(),所以 flush 看到的要么是默认 framebuffer,要么是它这一帧一直在画的动态分辨率目标。
- 实测 1280x720、202 栋建筑:CPU 4.14 → 3.47 ms/帧(-16%)。 图形侧只占 0.3 ms,这一帧是 CPU-bound,省下的就是净赚。 探针顺带测了 flush 内部各项(orphan 0.0001 / subdata 0.002 / 查询 0.000 / draw 0.009 ms 每次)——flush 本身很便宜,剩下的 ~4 ms 在 game 侧的 enqueue 循环里,尚未定位。
v0.1.8.1
建筑阴影质量 + 接收端查询整理 + 建筑动态材质系统 + 树木阴影修正 + 黄昏淡出。
黄昏淡出(太阳曲线的夜晚分支):
- 找到原版光照曲线的夜晚分支:
twilight * 0.65 + moon * 0.22(LightingSystem::effectiveSunIntensity 的 night 分支)。这段曲线原先只被 环境光读到,阴影完全看不到它。 - 日落后阴影渐隐而非瞬间消失:太阳一落到地平线以下,solarIrradiance 立刻 归零(辐照度曲线本身不建模民用暮光),所以旧代码里 19:00 阴影最长最重、 19:20 就彻底没了——全镇的影子一个 tick 内全部消失。现在把 sunTwilight 一起 喂给 sunShadow:日落后 30 分钟里,影长从 1.20 平滑降到 0,浓度从 0.414 降到 0,恰好随天空一起归零;深处的夜晚仍然是 0。
- 两侧对称:日出处同样从 0 渐入到 1.20,和日落完全镜像。
- CPU 接收端(body-vs-body)也走同一个 presence 因子,否则淡出的半小时里 "地面投影在变淡"和"站在影子里的建筑还没变暗"会各说各话。
- sunTwilight 单源化:原先 LightingSystem 里有一份私有拷贝和自己的窗宽常量, 环境光读那份、阴影读不到。现在一份函数两个消费者,曲线不可能再分叉。
树木阴影:
- 修正低画质档大树无影:多边形的门槛原先是 >0.5,而建筑用的是 >0.01, 于是 0.4 档的大树只拿到"树干的贴图拷贝"——一根棍子。现在两档一致, 实测 hq/morning 0.873 与 lq/morning 0.874 相同(改动前 lq 为 1.008,无影)。
- 修正阴影剔除距离减半:剔除测试原先取投射体自身矩形的高度,而建筑的 矩形是它的脚印(高的一半)、大树的矩形是树干(树冠在它上面)。所有阴影 的投射距离都被砍了一半——新增"镜头外树木的影仍落进画面"检查: 树在画面外 1.35(画面边缘 1.09),影内 0.3838 / 影外 0.4352 = 0.882。 用真实高度测约 1.0,也就是这一项的全部差别。
- 阴影视探针两个画质档现在都参与断言(原先 lq 只跑不查,理由已不存在)。
建筑动态材质:
- 建筑立面不再是一整张贴图一种反应:绘制时逐 texel 盖上材质部件 (墙/玻璃/木饰/铸铁/石基/织物/积雪),与 albedo 同尺寸同步烘焙成 伴随材质遮罩,精灵批处理把两者配对绑定,一个建筑的部件永远不吃 另一个建筑的材质。
- 每部件对世界光有自己的响应曲线:漫射权重(墙面平稳、屋顶摆动)、 天空承接(玻璃/积雪最高)、光泽(铸铁最亮最紧、砖石几乎不反)、 投影权重(墙面吃满、窗户保住镜中的天空)、夜间窗内灯光。
- 屋顶按受光面拆成四个部件:近坡(朝相机,只吃天空与低太阳)、 远坡(朝北,正午最亮)、东西两条戗坡(各吃早晚的低太阳)。 同一个相机画出来的屋顶因此会随时辰改变明暗,而不是整天一块亮。
- 新增 SunPath::dayLightFactor:天空亮度 0..1,整夜与两个地平线为 0、 白天为 1。玻璃窗灯与天空响应用它,而不用太阳高度——高度在午夜和 日出时都是 0,会把窗灯留到中午前、又在日落那一刻全部点亮。 现在灯整夜亮着,在早晨头一小时里熄灭,和真实的窗子一样。
- 高光随时间在部件上移动:法线倾角逐 texel 由遮罩 B 通道给出—— 整片玻璃在正午高太阳下一齐泛光、早晚转哑;落水管是圆管, 受光的那个 texel 每小时都不同,光斑沿管子走过去。
- 受影量随 sprite 下发(新增 occluded 顶点属性),不再整栋一起压暗; 无遮罩配对时按旧扁平结果渲染,行为可回退。
- 高光与天空承接都按天空强度缩放:环境光的"颜色"在清晨和正午是一样的, 不缩放就会让第一道曙光滑得像正午。现在清晨的滑闪只有正午的几分之一。
- 窗灯按玻璃自身的亮度加权,不再整体加一层固定暖色:否则亮片、暗片和 中间的窗棂都一样橙,一片窗墙读成橙色方块而不是"玻璃后面有房间"。 暗片保留一点漏光,不会整个陷进墙里。
- 玻璃法线摆动放宽到三分之一圈:一整片玻璃都在同一角度上,正午会整块 糊成一片惨白;真实釉面总有一条亮带,其余是暗的。
- 修正"永久潮湿的建筑":立面湿度原先取地面材料的 wetnessRetention, 木地板的建筑等于晴天也带着 10% 水膜——常年偏暗、偏亮(塑料感)。 现在只取 porosity × 天气表面湿度,晴天的墙面是干的。
- 新增各部件雨水响应:砌体喝饱变暗、石板/瓦屋顶排走只留光泽、 玻璃成珠基本不吃水、布雨棚吸水最多、积雪是 0(它融化而不是变湿)。 一场雨因此能读出来:石基 0.544、雨棚 0.451、窗玻璃仍保持 0.688。
建筑阴影:
- 遮挡比例连续化:受影面采样从 3 站加密到 12 站(屋脊线 + 半墙高各 6 站), 比例 0.65/0.35。原先按 1/3 量化,阴影扫过街道时在三级之间跳变。
- 角接触不算遮挡:受影样本恰好同时落在投射体两条边界面上时,光线只能沿棱滑过, 不再计入。正午 0.12 长度的阴影不会染红西侧紧邻的建筑。 单面接触仍是有效命中(X 范围重叠 + 水平太阳的建筑不会被漏染)。
- 接触阴影沿投影方向偏移 0.6 格:原先固定洒在建筑北侧,早晚把最暗的一半 放在受光侧;现在与多边形投影同向,两者读作同一束光。
- casterReach 单源化:投射距离=影长×最高实体,空实体(开放式建筑、无冠树) 直接不进投射集合,接收端不再逐个扫描。
- 接收端查询单源化为 CasterSet:建筑、实体、玩家共用同一套权重与自屏蔽规则, 三处重复循环合并;投射集合只存有效质量,附带建筑→索引映射。
- 凸包预留容量精确化,尖顶建筑每帧少一次再分配。
v0.1.8.0
人物阴影 + UI 性能优化 + 城镇碰撞与地面美化。
人物阴影:
- 逐肢体投影阴影:身体各部位按骨骼姿势投射剪影,手/脚/头发不参与。
- 人物阴影更短更淡,多肢体重叠不再糊成黑团;旧接触 blob 移除。
UI 性能:
- HUD 文本合批:SDF 文本批作用域 + rect/text 两遍提交,flush 从 70+ 次降到个位数。
- 字符串与布局缓存:百分比查表、文本高度免 measure、wrap 缓存、面板布局缓存;热路径 ostringstream 清除。
- ComplexUI:主题一次烘焙、effector 空转跳过、容器绘制序缓存、hit-test 复用、effector const 修正。
- 菜单文本 measure 缓存、面板细节 LOD、玻璃 blur 降频;新增 UiPerf 回归测试。
城镇(Ronesville):
- 碰撞:3x3 小建筑填满碰撞;门洞封平改为交互进入;南墙线北移一格,北面不加行。
- 选址:出生点 Flat 层偏好干城镇(前瞻 48 环),pad 无水与计时预算不断言不动。
- 地面美化:广场石材边框、路口斑马线;分区地面(市政石板/商业 dirt/工业石子/农场草地/市集夯土/住宅草坪);建筑地板按行当走;花坛避开工厂与商业核心。
v0.1.7.0
光照与阴影系统重做 + 建筑渲染 + 城镇美化。
光照/阴影:
- 新增屏幕空间遮挡阴影:阴影投射器先渲染到半分辨率 shadow map, 乘法累积 + 两遍高斯模糊后,主渲染按屏幕位置采样调制颜色。
- 东升西落:阴影沿光源反方向投射(早上太阳在东,阴影朝西)。
- Shear 拉伸投影:阴影底部固定在物体脚下,顶部沿光源反方向拉伸, 消除“纸片复制”感;远端淡出。
- 新增环境光统一项(颜色 + 强度),由昼夜/月相/暮光/天气驱动。
- 重叠阴影通过乘法累积正确变暗,不再出现堆叠死黑。
- 修复阴影 pass 后 VAO 未切回导致主画面错乱的问题。
建筑:
- 屋顶形状合理化(前后坡/侧坡重拼、屋脊加高、后房棱)。
- 所有房屋统一透视;修复屋顶侧面透明。
- 木门颜色压暗,不再抢眼。
城镇(Ronesville):
- 街道中央车辙线、路口全铺装、公园中央碎石小径、草地花园斑块、 广场四角树木;庭院加碎石斑点。
- 移除镇内小摆件(路灯/长椅/花箱/木箱/木桶/煤袋)与 forest_tree, 只保留 great_tree;测试同步更新。
- 各街区候选建筑调整(凉亭/校舍/砖厂分布)。