外观
人物尺度步行实验
本入口验证人物在既有城市网格上的移动与观察,默认使用中性代理;显式角色预览可选择曜或玲的灰阶骨骼候选,正式造型及可达内容范围由对应设计任务决定。地图、建筑、道路与台阶继续使用 source-assets/district-map/district.json,输入设计见输入与交互,命令见仓库 game/README.md
运行契约
正式入口追加 --walk-preview,场景准备任务通过 CollisionWorld::from_scene 合并 PreparedScene.parts 与已接入的静态 GLB 碰撞,再在 /nodes/home 向下扫掠确认胶囊有可行走支撑。必需碰撞网格或出生支撑失败沿用加载失败页;返回标题清理玩家、碰撞缓存与输入意图,下一次进入重新建立
查询采用固定的 Parry 0.30.2,只使用静态三角 BVH、胶囊和球体扫掠,不启动刚体世界。Bevy与Parry的向量版本通过数组边界转换,地图米制坐标不变;索引与无索引 TriangleList 均可接入,每段三角保留源对象映射,许可与版本见仓库 THIRD_PARTY_NOTICES.md
包含地形、道路及台阶、建筑外壳与派生门框、窗框、雨棚等立面构件、平台和结构设施;V-A08 四面立面、V-15 南西立面和屋顶长椅使用实际 GLB 三角,按与渲染相同的节点、资产尺度和放置变换接入。三者要求单 mesh 根和静态 TriangleList,读取或结构错误中止场景准备;其他导入道具、植被与水面仍不参与碰撞。月台杂货V-04首层公共区域使用真实门洞和室内结构,其他建筑沿用未开放的外壳;实验覆盖小店首层公共区、小店周边与短登高路线
小店首层公共区
V-04的 design.floors 中1F rooms 与 openings 是房间范围和开口的唯一源。接待与陈列、主题陈列、预约洽谈三间公共房开放步行,地板、隔墙、门头和天花生成到同一份 PreparedScene.parts,渲染与人体/相机碰撞共用实际几何
玩家从原出生点沿街道走过共用店门进入接待与陈列,再经对应门洞进入主题陈列或预约洽谈,按原路返回街道。进入和退出使用WASD/左摇杆,不需要F,不传送、不切场景,也不重建玩家或碰撞缓存;F仍只负责公共地点观察。后场、私人内梯与楼上未开放,由实际隔墙和楼板封闭
室内继续使用原胶囊与跟随镜头,门框、门头、隔墙与天花同时约束移动、跳跃和镜头扫掠。HUD在公共房间范围内显示房间名称,回到街道恢复附近地点标签;房间判定沿用源多边形与所在楼层高度,不以按下进入键或任务变量代替当前位置
walk-shop-interior.json 从真实出生点输入移动与转向,依次经过三间公共房并回到街道。检查真实脚点与室名、着地与高度、零异常恢复,以及门边/墙边相机距离;窄测另覆盖门洞净空、封闭区域阻挡、贴墙跳跃和室内顶面;低顶跳跃继续复用已有碰撞检查。画面检查关注进门构图、转身遮挡、室内可读性及来回过程,机器结果不代表角色或主观体验已获验收
灰阶角色动作预览
--character-preview 默认选择曜,显式 --character-preview=CHR-001 与其相同,--character-preview=CHR-002 选择玲;未知ID启动失败。两者均隐含 --walk-preview,复用同一地图、PlayerState、碰撞、输入和相机;默认步行入口继续使用胶囊,启动参数只选择技术候选,不增加运行中的角色切换玩法。character::install 将选定GLB挂到脚底视觉 root,米制 Y-up、局部 +Z 前向,移动后按实际碰撞结果转向;模型不提交世界位移
| 候选 | 运行资产 | 当前模型尺度与动作 |
|---|---|---|
曜 CHR-001 | game/assets/characters/CHR-001/yao-grey-study.glb | 约1.744m,32骨,Idle/Walk/Run/Jump均为in-place |
玲 CHR-002 | game/assets/characters/CHR-002/ling-grey-study.glb | 约1.636m(1.65m比例目标),32骨,Idle/Walk/Run/Jump均为in-place |
加载等待 GLB 完整依赖,再按 Scene 及 Idle / Walk / Run / Jump 名称建立场景与 AnimationGraph。实例化后核对一个 AnimationPlayer、蒙皮以及全部 clip 目标,真实就绪后才显示模型并隐藏胶囊;缺文件、缺动作、依赖或目标失败均保留胶囊并记录明确错误,加载或实例化超过30秒同样失败。capture等待真实ready,错误产生非零退出码,不把代理回退算作角色成功
接地且停止移动时播放Idle,有水平移动时按实际碰撞后速度选择Walk或Run;离地播放一次约0.667秒的in-place Jump,超出动作时长保留末姿态直到真实接地,世界升降仍完全由控制器处理;播放速率对应控制器速度,按住疾跑键但被完全挡住时不继续跑步动作。玲的骨架与平移曲线按模型高度同比缩放,Walk/Run的自然步速分别使用 3.2 × 1.65 / 1.7441906 与 5.6 × 1.65 / 1.7441906 m/s,播放倍率为实际速度除以对应自然步速,人物控制器的移动速度不变。暂停、观察、失焦及待切状态使用与控制器相同的操作门禁暂停 AnimationPlayer,继续后恢复时间推进;返回标题沿玩家 root 递归清理,重入重新加载和核对。CharacterStatus 在动画系统之后读取所选角色ID、实际ready、错误、当前动作、seek_time、elapsed、切换数、暂停和目标数量,capture不代写结果
两模型均为 needs_revision 灰阶研究,外观与配色未获作者或美术批准;上述高度是当前模型尺度,技术接入不表示角色美术已完成。当前直接切换动作,尚未制作混合过渡;步幅缩放只建立播放速度对应,Walk / Run 足滑与接地仍需逐模型实测,Jump目前为单次起落姿态,长距离下落、移动起跳与落地混合仍需后续制作
fish
cargo run --manifest-path game/Cargo.toml --locked --bin n-side -- --project-root . --character-preview
cargo run --manifest-path game/Cargo.toml --locked --bin n-side -- --project-root . --character-preview=CHR-002
cargo build --manifest-path game/Cargo.toml --locked --bin n-side
python3 tools/capture.py --binary game/target/debug/n-side --character-preview --script game/capture/walk-character.json --output output/capture/walk-character
python3 tools/capture.py --binary game/target/debug/n-side --character-preview CHR-002 --script game/capture/walk-character.json --output output/capture/walk-character-lingcapture包装脚本对所有角色ready帧核对所请求的稳定ID,缺少ready帧或加载另一角色均失败,检查结果单独标为wrapper来源。capture包装脚本同时接受 --character-preview CHR-002 与等号形式,不带值仍选曜;原生游戏命令的角色值使用等号形式。两模型复用同一份 walk-character.json 检查真实Idle、Walk、两种疾跑输入、键鼠/手柄Jump、落地、暂停与恢复,动作推进和暂停漂移断言与连续画面分别核对;各模型制作与运行结果见TASK-047,一方通过不代替另一方运行证据。脚本输出目录需为新目录,不以编译、DCC循环或播放时间单独放行人物外观
game/capture/character-jump-pause.json 专门覆盖空中暂停:冻结实际人物位置和Jump时间、恢复同一次起跳、落地返回Idle、再起跳重新播放。两位角色分别验证,脚本输入不改人物或动画状态
本轮参数
| 项目 | 实验值与职责 |
|---|---|
| 胶囊 | 总高1.7m、半径0.3m,脚点记录物理最低点 |
| 行走与疾跑 | 步行3.2m/s、疾跑5.6m/s,输入按镜头水平方向转换,摇杆径向死区0.15、对角限幅;左右Shift、右键或左摇杆按钮持续按住疾跑 |
| 跳跃 | 空格/手柄西键在有地面支撑时起跳,初速6m/s、重力18m/s²,理论顶点约1m;头顶与落地沿同一胶囊扫掠,不允许空中连跳,持键不连续起跳 |
| 碰撞 | 表面间隙0.02m,另留0.001m数值余量;每步最多4次扫掠滑动,近零接触时给投影后的剩余位移增加1µm向外分量再完整扫掠,避免切向舍入导致原地停滞 |
| 台阶 | 最大尝试抬升0.26m,依次验证头顶、前向空间与落点支撑 |
| 支撑 | 最小实际踏面法线Y为cos45°;圆帽命中共享棱边时先沿接触方向偏移10mm核对踏面,若失败,再沿几何边两侧各核对一次同范围实际踏面,避免高处近竖直接触的水平误差,重力18m/s²,行走时保留向下贴地范围 |
| 镜头 | 目标为脚点上方1.35m,期望距离3.8m,0.18m球体与0.04m间隙防穿墙;距离低于0.8m时隐藏代理,移远后恢复 |
| 镜头输入 | 默认锁定鼠标,M切换,解锁时左键拖动;Q E或右摇杆;俯仰限制约−7°至66° |
这些值用于原型比较,不构成作者确认的角色或关卡预算。角色使用 Bevy FixedUpdate;普通启动沿用Bevy默认64Hz固定时钟,capture将固定步长设为脚本fps,证据必须记录该值。输入先于固定循环采样,鼠标位移每渲染帧消费一次;固定时间不保证不同GPU的像素一致
检查与恢复
鼠标锁定意图与平台CursorOptions分开:进入步行默认请求锁定并隐藏,M切换请求,暂停、观察、失焦、标题与加载释放鼠标;恢复且旧输入释放后按先前请求重新锁定。原生后端不支持Locked时Bevy可回退Confined,逻辑capture与组件检查不能单独证明桌面捕获成功。空格与M忽略系统重复消息,跳跃意图只消费一次固定步,暂停冻结空中速度;灰阶候选的Idle/Walk/Run/Jump由上述角色预览独立管理
窄测验证薄墙高速扫掠、上阶及低顶阻挡、墙边离开,以及从实际小店节点沿道路网格移动;相机扫掠与人体扫掠分别保留检查。场景录制继续使用共享capture,不创建替代街景或脚本设定完成位置
脚本记录脚点、是否有支撑、碰撞源、恢复次数和相机距离,断言按真实路线声明范围;机器通过后检查连续画面的角色可见性、遮挡和台阶表现。walk-ascent-entry.json 覆盖小店至首段登高台阶,walks_complete_short_ascent_and_return 从真实出生点按源 hill-short 节点逐段给水平输入,再沿原路回店,分别检查64Hz和30Hz;CPU路线探针与GPU画面分别记录。walk-ascent-full.json 沿同一路线从真实入口录制完整上山过程,驱动仅观察玩家并发送常规输入,按节点留下状态与连续画面。walk-round-trip.json 继续覆盖同一会话的完整下山与回店,峰顶只计一次、共259次节点到达;零恢复和最后节点的真实位置共同约束回店。物理手柄、作者手感、动画品质和全城导航需要各自证据
R/Select执行显式回到已验证出生点;越出静态世界边界或出现非法位置时记录警告并执行同一路径,所有恢复增加计数。capture限制恢复次数,避免把自动复位当作正常走完。主窗口失焦先请求暂停,在固定更新前阻止移动、重力、贴地、镜头和复位;保留角色的速度和当前位置,继续后恢复同一会话。加载完成时仍失焦也走同一路径;无窗口录制不会因缺少窗口而暂停。未打开地点观察时Esc/Tab/Start/东键进入暂停,人物与镜头保留;观察面板的返回优先级见下文。暂停输入先于固定循环处理,开菜单当帧停止移动、转向与复位。继续时要求旧玩法输入释放后再操作,包括相抵的按键或摇杆输入;回焦本身及持有的确认输入不会继续游戏,系统自动重复的菜单按键忽略,释放后重新按下才能确认;返回标题退出实验
walk-pause.json 覆盖键盘与模拟手柄暂停、持有输入、继续、标题清理和再次进入;walk-focus.json追加模拟窗口焦点的加载/恢复路径,CPU调度测试另检查0/1/3个固定步、鼠标继续、代理实体唯一,以及真实输入插件处理的迟到重复按键。暂停不重建碰撞缓存,也不暂停Bevy全局时钟;灰阶角色动画按上述门禁暂停,后续NPC与声音仍需分别约定暂停职责
相关工作与本轮结果由 todo/tasks/TASK-014-human-scale-prototype.md 记录,长期保留通过验证的接口和已知边界
手柄连接由Bevy InputPlugin在PreUpdate处理,断连移除Gamepad但保留设备实体;入口在固定循环前消费同批连接消息,复用待暂停门禁冻结人物与镜头。当前所有连接设备都参与聚合输入,任一断开触发保护;不按最后输入设备排除其他手柄。加载时的断连请求延续至World首帧,标题/新加载/失败清除;恢复不重建代理或碰撞,连接与回焦共用菜单释放门槛。walk-gamepad.json与CPU调度测试检查模拟连接的实际行为,物理设备恢复另验
会话设置
标题和暂停菜单均提供「设置」,使用方向键/手柄方向键选择,Enter/南键A或点击按钮切换。通用页提供文字大小100%/125%与镜头灵敏度100%/65%,画质页按画质契约提供19项渲染与窗口选项;变化立即生效,返回标题、场景清理和再次进入保留选择,正常启动通过独立用户文件在下次运行恢复。该文件只保存显示与操作偏好,已实现的门前交接另用下文的进度文件
settings::SettingsStore 使用版本2的 settings.json,字段为 version、large_text、slow_camera 与 graphics,版本1自动补入默认画质。Linux优先使用绝对路径的 $XDG_CONFIG_HOME/N-SIDE/settings.json,否则使用 $HOME/.config/N-SIDE/settings.json;Windows使用 %LOCALAPPDATA%/N-SIDE/settings.json。--settings-dir DIRECTORY 可显式指定目录,源目录与运行素材不作为默认写入位置
首次没有文件时使用100%/100%,调整后由I/O任务保存;每帧末尾记录最新偏好,正常退出等待已启动的写入并提交之后的最后一次选择。替换前重新核对旧文件版本与字段,保留合法旧值为 settings.json.bak;临时文件采用独占创建,写入同步后重命名。保存期间显示「正在保存」,成功后才显示「已保存」;读取失败、超过4096字节、未知版本或字段、画质索引越界或互斥组合时保留原文件并仅在内存中调整,写失败保留当前文件并显示原因类别。失败后可退出程序核对文件、备份及目录,再修复并重新启动;不自动覆盖无法解释的设置
capture默认使用100%/100%的隔离会话,既不读取也不修改真实用户设置。仅显式传入 --settings-dir 时启用文件读写,跨进程复验使用独立临时目录;walk-settings.json 完成调整,第二次启动用 walk-settings-restore.json 核对125%/65%已加载,并以真实镜头转角验证65%档。断电恢复、Windows实机目录与权限由各自环境补充证据
文字大小作用于正式标题、暂停、设置、地点HUD与公共观察面板的真实TextFont,不缩放场景或生成文字贴图。横屏按既有1440逻辑宽度约束缩放,竖屏改用720逻辑宽度,面板按可用宽度换行;左上地点标签最多占可用宽度55%,给右上暂停按钮留出空间,左下操作说明可以使用完整宽度。字号、长中文与实际画幅的可读性仍需画面检查
镜头倍率在 player::read_input 对Q/E、鼠标观察位移和右摇杆统一生效,俯仰限位、移动速度及碰撞查询不变。自动路线读取同一个实际倍率,按当前转速限制预测下一帧朝向,再计算普通摇杆与鼠标输入;保持100%默认行为,65%时不会使用未缩放的朝向预测代替真实操作
设置是来源菜单中的页面,不改变GamePhase。Esc/东键B或「返回」回到原菜单并恢复原焦点,返回输入需释放后才能再次确认;从暂停进入设置时继续隔离人物和镜头,不把确认或返回同时交给玩法。公共观察打开时可先用Tab/Start暂停,再修改设置;关闭设置并明确继续后,恢复原选定地点的观察,并应用新字号。观察返回与暂停保护仍遵循下文的输入优先级
walk-settings.json 声明标题与暂停两处设置入口、返回焦点、字号与镜头倍率、设备切换、清场与重入检查;walk-observation-settings.json 将真实接近、公共观察、暂停设置、恢复、关闭、转镜头和再次进入连成一段。capture的 settings_open、entry_focus、text_scale、camera_sensitivity、entry_device 读取正式入口状态,断言检查整个声明区间;状态检查与宽/窄屏画面自查分别记录在任务证据,命令见仓库 game/README.md
地点提示
人物入口从同一地图读取场所名称和公共到达路径的末节点,没有公共到达路径时使用原场所位置。街道HUD显示水平32m、竖直4m内最近地点的「附近」标签,其他位置回退Null Site;小店首层公共房间内优先显示实际所在室名。稳定地点ID保持原数据,暂停与标题隐藏世界HUD,重入重新加载目录
左上地点与左下操作提示使用街区信号tokens,按当前实际输入设备切换;标签只在状态变化时更新。walk-places.json 通过真实行走验证离开、回店、暂停、键鼠/模拟手柄切换与清场;CPU检查高差边界及缺失公共到达节点的诊断
公共地点观察
places::observation 在人物入口提供近距离查看,当前开放月台杂货(地点ID 04)、摘星台(23)、店侧台阶(29)与货运侧院(28)。名称、用途、开放说明与到达方式直接读取同一地图的 name、use、time、entry;用途和到达说明必填,04/23另要求开放说明,28/29没有独立开放时段时不虚构内容。信息缺失沿用加载失败诊断;查看公共信息不表示已进入室内
门前交接通过 ShopHandoff 记录实际打开的04/29/28,允许任意观察顺序并去重;23仍是普通公共观察。三个地点都已观察后,在28面板明确选择配送路线,经过地点或打开面板本身不自动确认。选择货运侧院提交一次;选择店侧台阶显示推车无法通过的修订反馈,保持待选并可重试。记录只包含实际已观察的条目,HUD提示下一目标;返回标题、重新加载、场景失败与暂停均保留交接事实,跨进程恢复按下文执行,完整序章结果尚未实现
可选时上下方向键/手柄方向键切换,F/南键A确认,或点击对应按钮;首次打开、旧输入未释放或不在28时不能提交。标题、选项和返回控件固定在面板内,正文单独裁剪滚动,滚轮、PageUp/PageDown或右摇杆可回看长文本;正文滚动不带走路线选项。改变字号或画幅后按实际布局计算滚动范围
完整交接验收使用 game/capture/prologue-route.json,连续从真实出生点走到04→29→28,分别检查观察取得、错误选择修订、正确确认与重开后的幂等;CPU连续步行测试独立检查实际网格、台阶与支撑。固定时序输入需要随移动或地图变更复验,不能把状态单测或单地点画面称为完整交接已验收。任务与本轮证据见TASK-050
可查看目标必须满足:脚点到公共到达锚点的水平距离≤3m、高差≤1m;相机前向与相机至目标方向的单位向量点积≥0.75;从脚点上方1.35m到目标同高度的视线通过实际 CollisionWorld,半径0.015m的球体查询允许末端15cm接触目标表面(覆盖既有门立面的实际厚度),中间墙体仍阻挡。最近地点的32m HUD提示不等于满足观察条件,符合条件时才显示F/A提示
锁定时鼠标移动、解锁时左键拖动、Q E与右摇杆负责转动镜头;右键作为疾跑输入,F或手柄南键(A)打开公共信息面板,没有可查看目标时不会打开。面板持有选定地点,打开当帧、阅读期间及关闭当帧隔离人物移动、镜头和复位,关闭后旧玩法输入需要释放再操作;同一次开关输入不会穿透到其他操作
面板打开时Esc/东键(B)或鼠标左键点击「返回街区」只关闭观察,保持World,不触发暂停;空白处点击不关闭。返回按钮使用原生 Button 与 On<Pointer<Press>>,悬停由 Hovered 更新边框;点击请求交给同一输入门槛,关闭帧及继续按住鼠标时仍消费输入,释放后才能重新操作。入口因失焦或暂停提前返回时,未消费的点击在该帧输入处理后清除,不在恢复时补执行
Tab/Start仍可保护性暂停,窗口失焦或设备断连沿用相同保护。暂停暂时隐藏面板,明确继续后恢复原选定地点的观察;回到标题、重新加载或失败清空瞬时观察状态与选定地点,关闭或隐藏时销毁面板实体,ShopHandoff 中已取得的信息与确认结果保留
walk-observation.json 从真实小店出生点行走并转向公共入口,覆盖键盘打开/关闭、持键隔离、模拟手柄打开、断连暂停、键鼠继续恢复原观察、重连关闭、标题清理与再次进入。capture的 observation_target、observation_selected、observation_open、observation_visible 来自实际资源,声明这些断言的帧区间必须逐帧满足;距离、高差、背向和中间墙阻挡由CPU窄测覆盖,运行及画面结果另记任务证据
walk-observation-pointer.json 验证空白点击、真实返回按钮命中、持键隔离、释放后移动、模拟手柄重开、暂停及恢复后的鼠标返回。events.pointer 是捕获画布内的像素坐标,左上为原点;left_mouse 声明左键持有区间并要求显式坐标。capture在PreUpdate原生picking处理前发送同一图像目标的 PointerInput,经 UiPickingPlugin 的实际布局、裁剪和命中生成Pointer事件,再由输入路由关闭面板;另更新普通鼠标按键资源以复用等待释放门槛,不写 Interaction 或面板结果。该入口只证明使用Pointer事件的实际控件,标题及暂停Shell的其他鼠标按钮需单独验收
fish
cargo test --manifest-path game/Cargo.toml --locked --lib observation -- --nocapture
cargo build --manifest-path game/Cargo.toml --locked --bin n-side
python3 tools/capture.py --binary game/target/debug/n-side --script game/capture/walk-observation.json --output output/capture/walk-observation
python3 tools/capture.py --binary game/target/debug/n-side --script game/capture/walk-observation-pointer.json --output output/capture/walk-observation-pointer交接进度保存
progress::ProgressStore 只持久化当前门前交接的版本1检查点:quest 为 QST-001,checkpoint 为 home,observed_places 为不重复的04/29/28子集,confirmed_route 仅允许空值或三点齐备后的 service_yard。阶段由事实推导,确认计数恢复为0或1,错误选择次数仅属会话;人物坐标、镜头、观察面板、完整委托与世界状态不写入该文件
正常 --walk-preview 与隐含步行模式的 --character-preview 从用户目录读取 progress.json:Linux使用绝对路径的 $XDG_DATA_HOME/N-SIDE/progress.json,否则回退 $HOME/.local/share/N-SIDE/progress.json;Windows使用 %LOCALAPPDATA%/N-SIDE/progress.json。--progress-dir DIRECTORY 显式覆盖目录。固定镜头模式不读写交接进度;capture默认只有隔离会话,只有显式 --progress-dir 启用文件持久化,--settings-dir 不改变这一规则
首次无文件时从空检查点开始,实际观察或路线确认改变事实后在 Last 提交I/O任务,正常退出等待已启动的写入并保存随后产生的最新事实。复用设置文件的4096字节读取上限与原子写入:替换前校验旧文件,将合法旧值备份为 progress.json.bak,临时文件独占创建、同步后重命名。读取失败、未知字段或版本、非法地点及矛盾结果保留原文件,本次禁用进度写入;写失败保留旧文件与当前会话,状态明确显示失败,不在事实未变化时逐帧重试。两类失败都应在退出后核对文件、备份及目录,修复后重新启动;不自动从备份覆盖原文件
标题按事实显示「开始门前交接」或「继续门前交接」,每次进入都从 /nodes/home 的实际支撑点重新创建人物;暂停的「继续」则保留当前人物与观察位置。已有观察时,标题和暂停增加「重新开始交接」:确认页默认「保留当前进度」,取消回到原焦点,持有进入确认页的输入不会继续提交;另选「确认重新开始」才清除事实、自动保存新检查点并重建场景。读取失败时重新开始仅清除当前会话,不覆盖受保护的原文件
复验使用真实观察产生的独立测试存档,prologue-save-write.json 与第二进程的 prologue-save-restore.json 覆盖单点保存、继续、取消与确认重开;完整路线生成的存档交给 prologue-save-confirmed.json 检查三点结果及幂等。损坏与写入失败脚本必须先按对应任务证据准备专用目录,不能操作真实用户文件或注入已完成的游戏事实。执行命令见工程入口,本轮运行结果与环境限制见TASK-051