# CMR Mini 开发架构阶段总结 > 文档版本:v1.20 > 最后更新:2026-04-03 19:26:23 文档维护约定: - 仓库内 Markdown 文档统一在标题下方标注 `文档版本` 和 `最后更新`。 - `最后更新` 必须写到日期时间,例如 `2026-04-02 08:28:05`。 - 后续新建文档或更新文档内容时,必须同步更新这两项元信息。 本文档用于记录当前阶段小程序的整体架构、分层原则、事件驱动链路、模拟器体系,以及后续继续扩展时应遵守的边界。 当前补充约定: - 多线程联调场景下,正式架构与长期结论优先沉淀到 `doc/`。 - 当前联调架构的阶段总结见: - [联调架构阶段总结](D:/dev/cmr-mini/doc/gameplay/联调架构阶段总结.md) - 活动列表最小产品方案见: - [活动卡片列表最小产品方案](D:/dev/cmr-mini/doc/gameplay/活动卡片列表最小产品方案.md) - 面向后端线程的阶段性实施说明,优先写入根目录 [t2b.md](D:/dev/cmr-mini/t2b.md)。 - backend 新增写给总控线程的回写板: - [b2t.md](D:/dev/cmr-mini/b2t.md) - 面向前端线程的阶段性实施说明,优先写入根目录 [t2f.md](D:/dev/cmr-mini/t2f.md)。 - frontend 写给总控线程的回写板: - [f2t.md](D:/dev/cmr-mini/f2t.md) - 分层原则固定为: - 玩家用前端 - 管理者用后端 - 中间层负责契约、架构、性能、健壮性与伸缩性 - 不把后台复杂性直接暴露给玩家界面 - 后台生产闭环的正式架构稿见: - [后台生产闭环架构草案](D:/dev/cmr-mini/doc/backend/后台生产闭环架构草案.md) - 正式上线时的数据库与服务发布流程见: - [生产发布与数据库上线方案](D:/dev/cmr-mini/doc/backend/生产发布与数据库上线方案.md) - backend 下一阶段建议: - runtime 链已收口,frontend 当前不再扩 runtime 页面链 - 活动运营域第二阶段第四刀已完成: - `EventPresentation` 统一导入入口 - `Event` 默认 active 三元组固化 - publish 默认继承 active 三元组 - 当前主线已切到“联调标准化阶段” - 当前已完成: - `GET /events/{eventPublicID}` 透出 `currentPresentation / currentContentBundle` - `GET /events/{eventPublicID}/play` 透出 `currentPresentation / currentContentBundle` - `launch` 透出 `presentation / contentBundle` - publish 可自动补齐 `presentationId / contentBundleId` - `release detail` 已统一活动运营摘要 - `ContentBundle` 统一导入入口第一版已完成 - `Bootstrap Demo` 已可补齐: - `place / map asset / tile release / course source / course set / course variant / runtime binding` - `一键补齐 Runtime 并发布` 已可从空白状态跑完整测试链 - `一键标准回归` 与 `回归结果汇总` 已接入 workbench - `当前 Launch 实际配置摘要` 已接入 workbench - `前端调试日志` 已接入 workbench - 三类标准 demo 入口已显式挂出: - `evt_demo_001` - `evt_demo_score_o_001` - `evt_demo_variant_manual_001` - workbench 日志已具备: - 分步日志 - 真实错误 - stack - 最后一次 curl - 预期判定 - 下一步建议: - 联调标准化第一版视为已完成 - 真实输入替换第二刀已完成: - `content manifest` - `presentation schema` - `活动文案样例` - 活动卡片列表最小产品化第一刀已完成 - 当前主线进入“活动卡片列表第一刀联调回归与小范围修复阶段” - backend 当前应优先保证: - 从空白环境直接可跑 - workbench 日志能明确定位失败步骤 - 同一条测试链可重复执行 - backend 当前分工: - 维护活动卡片列表第一刀所需最小摘要字段稳定 - 响应列表页联调中暴露的字段、默认值和语义问题 - 保持列表页与活动详情页摘要口径一致 - 前端线程建议正式上场时机: - 现在已完成活动运营域摘要接线第一刀 - 当前已完成: - runtime 摘要链: - 准备页预览态摘要 - 地图页 - 单局结果页 - 历史结果列表页 - 首页 ongoing - 首页 recent - 活动运营域摘要链: - 活动详情页 - 活动准备页 - 会话快照 - 当前建议: - frontend 已完成活动卡片列表最小产品化第一刀 - frontend 当前进入联调回归与小范围修复阶段 - 优先复用 backend 一键测试环境做回归 - 优先复用: - `回归结果汇总` - `当前 Launch 实际配置摘要` - `前端调试日志` - 当前不扩更多玩家侧新链 - 不重做首页现有入口区 - 不做复杂运营样式 - frontend 当前分工: - 活动列表页第一刀回归与小修 - 结构化日志补充 - 配合 backend 收口字段与默认值 当前阶段的核心目标已经从“把地图画出来”升级为“建立一套可长期扩展的运动地图游戏底座”。 这套底座已经具备以下关键能力: - 地图引擎与玩法规则解耦 - 通用 telemetry 信息层独立 - 声音 / 震动 / UI 动效统一走反馈层 - 真实与模拟 GPS 双源并存 - 真实与模拟心率双源并存 - 顺序赛与积分赛两套规则可共存 - 外部模拟器可以在室内驱动定位与心率联调 --- ## 1. 项目当前总体分层 当前工程可以概括为 6 层: 1. 内容与配置层 2. 地图引擎层 3. 规则运行时层 4. Telemetry 信息层 5. Presentation 展示适配层 6. Feedback 反馈层 此外,还有一条独立的开发调试链: - 小程序调试面板 - 外部模拟器 `tools/mock-gps-sim` 这两者不参与业务规则,只作为开发辅助工具。 --- ## 2. 目录结构说明 ### 2.1 小程序主目录 位于 [miniprogram](D:/dev/cmr-mini/miniprogram): - [engine](D:/dev/cmr-mini/miniprogram/engine) - [game](D:/dev/cmr-mini/miniprogram/game) - [pages](D:/dev/cmr-mini/miniprogram/pages) - [utils](D:/dev/cmr-mini/miniprogram/utils) - [assets](D:/dev/cmr-mini/miniprogram/assets) ### 2.2 游戏相关目录 位于 [miniprogram/game](D:/dev/cmr-mini/miniprogram/game): - [core](D:/dev/cmr-mini/miniprogram/game/core):规则运行时核心模型 - [rules](D:/dev/cmr-mini/miniprogram/game/rules):玩法插件 - [presentation](D:/dev/cmr-mini/miniprogram/game/presentation):展示适配状态 - [telemetry](D:/dev/cmr-mini/miniprogram/game/telemetry):通用信息层 - [feedback](D:/dev/cmr-mini/miniprogram/game/feedback):反馈统一入口 - [audio](D:/dev/cmr-mini/miniprogram/game/audio):声音层 - [content](D:/dev/cmr-mini/miniprogram/game/content):课程内容转游戏定义 ### 2.3 地图引擎目录 位于 [miniprogram/engine](D:/dev/cmr-mini/miniprogram/engine): - [map](D:/dev/cmr-mini/miniprogram/engine/map):宿主编排与地图主入口 - [renderer](D:/dev/cmr-mini/miniprogram/engine/renderer):WebGL/2D 渲染 - [sensor](D:/dev/cmr-mini/miniprogram/engine/sensor):GPS、心率、罗盘、模拟源 - [tile](D:/dev/cmr-mini/miniprogram/engine/tile):瓦片缓存与加载 - [camera](D:/dev/cmr-mini/miniprogram/engine/camera):相机与坐标换算 - [overlay](D:/dev/cmr-mini/miniprogram/engine/overlay):地图叠加层相关 - [layer](D:/dev/cmr-mini/miniprogram/engine/layer):图层定义 ### 2.4 外部模拟器目录 位于 [tools/mock-gps-sim](D:/dev/cmr-mini/tools/mock-gps-sim): - [server.js](D:/dev/cmr-mini/tools/mock-gps-sim/server.js):本地 HTTP + WebSocket 服务 - [public/index.html](D:/dev/cmr-mini/tools/mock-gps-sim/public/index.html):新版模拟器工作台 UI - [public/simulator.js](D:/dev/cmr-mini/tools/mock-gps-sim/public/simulator.js):地图、路径、心率、日志、多通道模拟逻辑 - [public/workbench.css](D:/dev/cmr-mini/tools/mock-gps-sim/public/workbench.css):新版工作台布局与样式 --- ## 3. 设计原则 ### 3.1 地图引擎只负责地图能力 地图引擎负责: - 地图缩放、平移、旋转 - 真实 GPS 与模拟 GPS 的接入 - 真实心率带与模拟心率的接入编排 - 路线、控制点、轨迹、编号的绘制 - WebGL 与 2D 文本层的渲染同步 - 把规则层产出的 map presentation 转成地图显示 地图引擎不负责: - 当前玩法规则判定 - 计分 - 完成条件 - 打点策略 - 游戏胜负 核心入口文件是 [mapEngine.ts](D:/dev/cmr-mini/miniprogram/engine/map/mapEngine.ts)。 ### 3.2 规则层只负责玩法状态推进 规则层只关心: - 游戏事件输入 - 当前 session state - 规则推进后的 next state - 产出展示所需的 presentation - 产出反馈所需的 effects 当前已实现两种玩法: - `classic-sequential` - `score-o` 对应规则文件: - [classicSequentialRule.ts](D:/dev/cmr-mini/miniprogram/game/rules/classicSequentialRule.ts) - [scoreORule.ts](D:/dev/cmr-mini/miniprogram/game/rules/scoreORule.ts) ### 3.3 通用运行信息独立为 telemetry Telemetry 层不属于具体玩法,也不属于地图引擎。 它负责: - 用时 - 距离 - 当前速度 - 平均速度 - 当前目标距离 - 心率 - 卡路里 - 精度 - HUD 的通用体能颜色分级 入口文件: - [telemetryRuntime.ts](D:/dev/cmr-mini/miniprogram/game/telemetry/telemetryRuntime.ts) ### 3.4 展示状态独立为 presentation 当前展示层已经拆成两块: - `map presentation` - `hud presentation` 这样规则层可以分别决定: - 地图该怎么画 - HUD 该显示什么 而不让渲染器自己猜玩法语义。 文件: - [presentationState.ts](D:/dev/cmr-mini/miniprogram/game/presentation/presentationState.ts) - [mapPresentationState.ts](D:/dev/cmr-mini/miniprogram/game/presentation/mapPresentationState.ts) - [hudPresentationState.ts](D:/dev/cmr-mini/miniprogram/game/presentation/hudPresentationState.ts) ### 3.5 反馈统一走 effect 消费 声音、震动、UI 动效不是直接写在规则里,也不是直接写在页面里,而是由规则层产出 `GameEffect[]`,再交给反馈层消费。 反馈层入口: - [feedbackDirector.ts](D:/dev/cmr-mini/miniprogram/game/feedback/feedbackDirector.ts) 目前已经挂入: - 声音层 - 震动层 - 页面动效层 - 地图瞬时特效层 --- ## 4. 运行主链路 当前整套系统的主链路如下: ```text 远程配置 / KML / 静态内容 -> GameDefinition -> 传感输入 (GPS / 心率 / 罗盘 / 模拟源) -> MapEngine 编排 -> GameRuntime / RulePlugin -> GameSessionState + Presentation + Effects -> Renderer / HUD / FeedbackDirector ``` 更细一点可以拆成两条并行链。 ### 4.1 模块关系图 ```mermaid flowchart LR RC["远程配置 / KML / 静态内容"] --> GD["GameDefinition"] GD --> GR["GameRuntime / RulePlugin"] GPS["真实 GPS"] --> LC["LocationController"] MGPS["模拟 GPS"] --> LC BLE["真实心率带"] --> HR["HeartRateInputController"] MHR["模拟心率"] --> HR COMP["罗盘 / Heading"] --> ME["MapEngine"] LC --> ME HR --> ME ME --> GE["GameEvent"] GE --> GR ME --> TE["TelemetryEvent"] TE --> TR["TelemetryRuntime"] GR --> GS["GameSessionState"] GR --> GP["GamePresentation"] GR --> FX["GameEffect[]"] GS --> ME GP --> ME TR --> TP["TelemetryPresentation"] TP --> HUD["HUD / 状态色 / 体能面板"] ME --> RENDER["Renderer / WebGL / Label Canvas"] FX --> FB["FeedbackDirector"] FB --> SOUND["声音"] FB --> HAPTIC["震动"] FB --> UIFX["UI / 地图特效"] EXT["外部模拟器"] --> MGPS EXT --> MHR ``` ### 4.2 玩法链 ```text 地图点击 / GPS 更新 / 打点按钮 -> GameEvent -> GameRuntime.dispatch() -> RulePlugin.reduce() -> nextState + presentation + effects ``` ### 4.3 信息链 ```text GPS / 心率 / session 状态 -> TelemetryEvent -> TelemetryRuntime -> TelemetryState -> TelemetryPresentation -> HUD / 颜色 / 卡路里 / 距离 ``` --- ## 5. 核心模块说明 ## 5.1 MapEngine 文件: - [mapEngine.ts](D:/dev/cmr-mini/miniprogram/engine/map/mapEngine.ts) 职责: - 宿主编排器 - 不直接写玩法判断 - 管理地图视图状态 - 编排 `LocationController` - 编排 `HeartRateInputController` - 编排 `CompassHeadingController` - 编排 `GameRuntime` - 编排 `TelemetryRuntime` - 编排 `FeedbackDirector` - 汇总成 `MapEngineViewState` 透传给页面 这里是全局协调中心,但不是业务大杂烩。 应继续坚持: - 新玩法不要往这里塞规则逻辑 - 新传感器/模拟器可以继续从这里编排接入 ## 5.2 GameRuntime 文件: - [gameRuntime.ts](D:/dev/cmr-mini/miniprogram/game/core/gameRuntime.ts) 职责: - 载入 `GameDefinition` - 根据 `mode` 解析具体规则插件 - 对外提供统一 `dispatch(event)` - 维护: - `state` - `presentation` - `mapPresentation` - `hudPresentation` 当前支持: - 顺序打点规则 - 积分赛规则 这是后续继续加玩法的入口。 ## 5.3 TelemetryRuntime 文件: - [telemetryRuntime.ts](D:/dev/cmr-mini/miniprogram/game/telemetry/telemetryRuntime.ts) 职责: - 消费 `gps_updated` - 消费 `heart_rate_updated` - 同步 session 状态 - 计算: - elapsed - mileage - distanceToTarget - speed - averageSpeed - calories - heart rate tone 当前心率 / 速度逻辑: - 有心率时,颜色和卡路里优先走心率逻辑 - 无心率时,自动回落到速度代理区间 ## 5.4 FeedbackDirector 文件: - [feedbackDirector.ts](D:/dev/cmr-mini/miniprogram/game/feedback/feedbackDirector.ts) 职责: - 消费 `GameEffect[]` - 分发到: - `SoundDirector` - `HapticsDirector` - `UiEffectDirector` 当前后台音频方案已经暂时回退成前台-only。 原因是小程序后台音频 loop 行为不稳定,当前阶段不再强行实现。 ## 5.5 LocationController 文件: - [locationController.ts](D:/dev/cmr-mini/miniprogram/engine/sensor/locationController.ts) 职责: - 管理真实 GPS 与模拟 GPS 双源 - 暴露统一的位置更新 - 管理 mock bridge URL、连接状态、调试状态 当前支持: - `real` - `mock` 并且 mock GPS 已经可以通过外部模拟器驱动。 ## 5.6 HeartRateInputController 文件: - [heartRateInputController.ts](D:/dev/cmr-mini/miniprogram/engine/sensor/heartRateInputController.ts) 这是最近新增的一层,职责是统一真实心率带与模拟心率源。 它内部编排: - `HeartRateController`:真实 BLE 心率带 - `MockHeartRateBridge`:模拟心率桥 对上游暴露统一接口: - 心率值 - 状态文本 - 连接状态 - 设备列表 - 源模式调试状态 当前支持: - `real` - `mock` 这样 telemetry 和 HUD 不需要知道心率是从 BLE 来的还是模拟器来的。 --- ## 6. 玩法层当前状态 ### 6.1 顺序赛 classic-sequential 核心特征: - 开始点先打 - 打完开始点才显示完整路线 - 按顺序推进控制点 - 最后打终点结束 地图语义: - 单目标高亮 - 当前目标点闪动 - 当前目标腿流动动画 - 已完成点和线灰化 ### 6.2 积分赛 score-o 核心特征: - 自由选择未完成点 - 所有未收集点可见且高亮 - 用户可点击某个点设为 focus - HUD 显示选中点或最近未完成点距离 - 终点可随时选中并结束比赛 地图语义: - 不显示顺序导航腿 - 所有未完成点为多目标态 - 选中点有额外强化动画 - 圈内数字当前默认直接使用序号作为积分数字 ### 6.3 modeState 设计 当前规则层已经预留并开始使用 `modeState`。 含义: - 通用 `GameSessionState` 只放跨玩法共用字段 - 各玩法自己的私有状态放 `modeState` 这为后续新增玩法提供了稳定扩展入口。 --- ## 7. 当前 HUD 设计 目前 HUD 有两屏: ### HUD 1 - 打点主信息 - 用时 - 里程 - 当前目标距离 - 当前速度 - 打点进度 ### HUD 2 - 心率 - 卡路里 - 平均速度 - 精度 - 心率区间名称与区间说明 HUD 当前颜色由 telemetry 驱动。 规则: - 有心率:按心率分区 - 无心率:按速度代理区间 fallback 当前 6 档对应: - 蓝:激活放松 - 紫:动态热身 - 绿:脂肪燃烧 - 黄:糖分消耗 - 橙:心肺训练 - 红:峰值锻炼 --- ## 8. 心率带体系当前设计 当前心率带链路已经相对完整。 ### 8.1 单设备连接,多设备发现 当前不是“多设备同时连接”,而是: - 扫描发现多条设备 - 用户手选一条连接 - 连接成功后成为首选设备 - 后续自动回连只针对这条首选设备 原因: - 避免误连附近别人的心率带 - 保持 telemetry 和 HUD 语义简单 ### 8.2 首选设备持久化 已实现: - 首选设备持久化到本地 storage - 重进页面后仍可识别 - 自动回连优先使用首选设备 ### 8.3 BLE 断连重连处理 这一段实现过多轮修正,目前逻辑重点是: - 手动断开与意外断开分离 - 再连接前,先清理 BLE 残留连接 - 必要时刷新 Bluetooth Adapter - 再重新扫描 / 建连 这是目前心率带重连稳定的关键。 --- ## 9. 模拟器体系当前设计 ### 9.1 模拟器总目标 解决“GPS / 心率类 App 每次都要出去跑才能测”的问题。 当前外部模拟器已经支持: - 加载 `game.json` - 加载瓦片 - 加载 KML 控制点 - 地图点击与拖动 - 实时 GPS 发送 - 路径编辑与回放 - 导入轨迹文件回放 - 心率模拟发送 ### 9.2 GPS 模拟 消息协议: ```json { "type": "mock_gps", "timestamp": 1711267200000, "channelId": "runner-a", "lat": 31.2304, "lon": 121.4737, "accuracyMeters": 6, "speedMps": 2.4, "headingDeg": 135 } ``` ### 9.3 心率模拟 消息协议: ```json { "type": "mock_heart_rate", "timestamp": 1711267200000, "bpm": 148 } ``` ### 9.4 当前心率模拟能力 外部模拟器当前支持: - 固定 BPM 发送 - 连续发送 - 六档分区样本 - 真实样本模式 真实样本模式又细分成: - 慢跑样本 - 节奏跑样本 - 间歇跑样本 - 恢复走样本 ### 9.5 模拟器布局原则 当前已经调整为: - 顶部显示全局连接状态与全局模拟通道号 - 左侧控制面板独立滚动 - 中间地图固定作为主观察区 - 右侧保留运行摘要、当前位置、最近事件 - 右下使用可缩放的调试日志浮层 这样更适合长面板配置、多人联调隔离和过程日志观察,不会让地图区跟着滚动。 --- ## 10. 当前事件驱动模型 当前系统已经较明确地进入了事件驱动模型。 ### 10.1 规则事件 规则层的输入是 `GameEvent`。 典型事件: - `session_started` - `gps_updated` - `punch_requested` - `control_focused` - `session_ended` 规则层输出: - `nextState` - `presentation` - `effects` ### 10.2 Telemetry 事件 Telemetry 层的输入是 `TelemetryEvent`。 典型事件: - `session_state_updated` - `target_updated` - `gps_updated` - `heart_rate_updated` ### 10.3 反馈事件 反馈层消费的是 `GameEffect[]`。 这样: - 声音 - 震动 - UI 动效 - 地图脉冲 都可以走统一 effect 通道,而不是各处散写。 --- ## 11. 当前字段归属原则 这是当前项目后续扩展最重要的边界之一。 ### 11.1 放 Telemetry 适合放: - 速度 - 距离 - 心率 - 卡路里 - 精度 - 当前目标距离 这些是通用运行信息。 ### 11.2 放 GameSessionState 适合放: - 当前玩法状态 - 已完成点 - 当前目标点 - 得分 - 玩法私有状态 这些会影响规则推进。 ### 11.3 放 Presentation 适合放: - HUD 文案 - 当前激活腿 - 哪些点高亮 - 哪些点闪动 - 按钮状态 这些只是为了显示。 后面开发新增字段时,必须先判断它属于哪层,避免再次耦合。 --- ## 12. 当前已验证成立的扩展能力 当前架构已经通过以下场景验证过方向正确: 1. 从单一顺序赛扩成顺序赛 + 积分赛 2. 从真实 GPS 扩成真实 + 模拟 GPS 3. 从真实心率带扩成真实 + 模拟心率 4. 从单一 HUD 扩成双 HUD 5. 从单一音效扩成统一 feedback 模型 6. 从单设备心率带扩成多设备发现 + 单设备选择 这说明当前架构不是只能跑一个 Demo,而是已经具备继续扩展的基础。 --- ## 13. 当前已知现实约束 ### 13.1 微信开发工具层级模拟不可靠 目前已经确认: - 真机正常 - 开发工具在 `webgl canvas + 普通 view` 上的层级模拟并不稳定 因此后续验收原则是: - 真机为准 - 开发工具仅做辅助观察 ### 13.2 小程序后台音频能力有限 已经尝试过后台 guidance 音频方案,但当前阶段决定先回退: - 前台音频正常 - 后台不再强做 loop guidance 后续如果重新开启,需要: - 更适合循环的音频素材 - 更稳定的后台音频策略 ### 13.3 BLE 生命周期比 JS 状态更慢 心率带重连问题已经证明: - 逻辑没错,不等于 BLE 底层状态已释放 所以后续涉及 BLE 时,必须继续保留: - 清场 - 延迟 - 状态同步 --- ## 14. 当前阶段的结论 到目前为止,这个项目已经完成了从“功能堆叠”到“可扩展结构”的第一阶段转变。 现阶段最重要的成果不是某一个按钮或某一个玩法,而是以下架构能力已经成立: - 地图引擎与规则解耦 - 规则事件驱动 - telemetry 独立成层 - presentation 拆成 map/hud - feedback 统一消费 effects - 真实 / 模拟传感器源可插拔 - 外部模拟器可驱动整条业务链 这意味着后续继续开发时: - 新玩法主要加 rule plugin - 新通用统计主要加 telemetry - 新反馈主要加 effect 消费端 - 新传感器 / 模拟器主要加 sensor source 而不应该再把所有逻辑堆回 `MapEngine` 或页面里。 --- ## 15. 后续建议 当前阶段之后,建议按以下优先级推进: 1. 继续增加玩法插件 2. 增强调试面板与模拟器联调能力 3. 给更多玩法留 `modeState` 专属状态 4. 逐步丰富 telemetry 项 5. 继续打磨地图引擎的路线渲染能力 但无论怎么扩,建议始终遵守本文档里的边界原则。 --- ## 16. 新玩法扩展流程 后续新增玩法时,建议始终按下面这条路径落地,而不是直接往 `MapEngine` 或页面里塞逻辑。 ```mermaid flowchart TD A["新增玩法需求"] --> B["确定规则目标
完成条件 / 得分 / 失败条件 / 解锁逻辑"] B --> C["定义 modeState
只放玩法私有状态"] C --> D["新增 RulePlugin
reduce + buildPresentation"] D --> E["补充 GameDefinition 配置项
让远程配置可描述此玩法"] E --> F["拆 presentation
map / hud 分别表达"] F --> G{"是否需要新通用统计?"} G -- "是" --> H["扩 TelemetryRuntime
仅加入跨玩法可复用信息"] G -- "否" --> I["保持 telemetry 不动"] H --> J{"是否需要新反馈?"} I --> J J -- "是" --> K["新增 GameEffect 消费端
声音 / 震动 / UI / 地图特效"] J -- "否" --> L["保持 feedback 不动"] K --> M["调试面板补入口
只做该层对应的调试能力"] L --> M M --> N["外部模拟器补样本
仅在确有联调价值时添加"] N --> O["真机联调与验收"] ``` ### 16.1 新玩法落地时的边界检查 新增玩法前,建议先问下面几个问题: 1. 这是规则状态,还是通用信息? - 规则状态放 `modeState` - 通用信息放 `telemetry` 2. 这是地图显示语义,还是 HUD 文案语义? - 地图相关放 `map presentation` - HUD 相关放 `hud presentation` 3. 这是玩法逻辑,还是地图能力? - 玩法逻辑放 rule plugin - 地图能力放 engine / renderer 4. 这是单玩法专属能力,还是跨玩法复用能力? - 专属能力优先局部实现 - 复用能力再提升为全局层 ### 16.2 建议的新增玩法最小模板 后续新增一个玩法时,建议至少补这些文件或模块: - `game/rules/Rule.ts` - `game/content/courseToGameDefinition.ts` 中对应模式的定义装配 - `game/core/gameDefinition.ts` 中新增 mode 支持 - `game/presentation/*` 中新增该玩法需要的 map/hud 字段 - `pages/map/map.wxml` 只在确有显示需求时接新 HUD 文案 - 调试面板里补最小测试入口 如果一个新玩法一上来就需要大改: - `MapEngine` - `TelemetryRuntime` - `Renderer` 那通常说明这次设计边界还没想清楚,应该先回头重审玩法抽象。 --- ## 17. 关键文件索引 ### 地图与宿主 - [mapEngine.ts](D:/dev/cmr-mini/miniprogram/engine/map/mapEngine.ts) - [map.ts](D:/dev/cmr-mini/miniprogram/pages/map/map.ts) - [map.wxml](D:/dev/cmr-mini/miniprogram/pages/map/map.wxml) ### 规则层 - [gameRuntime.ts](D:/dev/cmr-mini/miniprogram/game/core/gameRuntime.ts) - [classicSequentialRule.ts](D:/dev/cmr-mini/miniprogram/game/rules/classicSequentialRule.ts) - [scoreORule.ts](D:/dev/cmr-mini/miniprogram/game/rules/scoreORule.ts) ### Presentation - [presentationState.ts](D:/dev/cmr-mini/miniprogram/game/presentation/presentationState.ts) - [mapPresentationState.ts](D:/dev/cmr-mini/miniprogram/game/presentation/mapPresentationState.ts) - [hudPresentationState.ts](D:/dev/cmr-mini/miniprogram/game/presentation/hudPresentationState.ts) ### Telemetry - [telemetryRuntime.ts](D:/dev/cmr-mini/miniprogram/game/telemetry/telemetryRuntime.ts) - [telemetryConfig.ts](D:/dev/cmr-mini/miniprogram/game/telemetry/telemetryConfig.ts) ### Feedback - [feedbackDirector.ts](D:/dev/cmr-mini/miniprogram/game/feedback/feedbackDirector.ts) - [soundDirector.ts](D:/dev/cmr-mini/miniprogram/game/audio/soundDirector.ts) ### 传感层 - [locationController.ts](D:/dev/cmr-mini/miniprogram/engine/sensor/locationController.ts) - [heartRateController.ts](D:/dev/cmr-mini/miniprogram/engine/sensor/heartRateController.ts) - [heartRateInputController.ts](D:/dev/cmr-mini/miniprogram/engine/sensor/heartRateInputController.ts) - [mockLocationBridge.ts](D:/dev/cmr-mini/miniprogram/engine/sensor/mockLocationBridge.ts) - [mockHeartRateBridge.ts](D:/dev/cmr-mini/miniprogram/engine/sensor/mockHeartRateBridge.ts) ### 外部模拟器 - [server.js](D:/dev/cmr-mini/tools/mock-gps-sim/server.js) - [index.html](D:/dev/cmr-mini/tools/mock-gps-sim/public/index.html) - [simulator.js](D:/dev/cmr-mini/tools/mock-gps-sim/public/simulator.js) - [workbench.css](D:/dev/cmr-mini/tools/mock-gps-sim/public/workbench.css) --- ## 18. 调试与模拟体系 当前项目已经不再依赖“必须出门跑一遍”才能测通主流程。 围绕调试和联调,已经形成了两条互补链路: - 小程序内调试面板 - 外部模拟器 二者是互补关系,不是替代关系。 ### 18.1 调试体系关系图 ```mermaid flowchart LR DEV["开发者"] --> MP["小程序调试面板"] DEV --> SIM["外部模拟器"] MP --> MGPS["切换 mock GPS"] MP --> MHR["切换 mock 心率"] MP --> MODE["切换玩法 / 调试动作"] SIM --> WS["WebSocket 桥"] WS --> GPSMSG["mock_gps"] WS --> HRMSG["mock_heart_rate"] GPSMSG --> LC["LocationController"] HRMSG --> HRC["HeartRateInputController"] LC --> ME["MapEngine"] HRC --> ME MODE --> ME ME --> GR["GameRuntime"] ME --> TR["TelemetryRuntime"] GR --> HUD["HUD / 地图渲染"] TR --> HUD DEV --> REAL["真机联调"] REAL --> BLE["真实心率带"] REAL --> GPS["真实 GPS"] BLE --> HRC GPS --> LC ``` ### 18.2 小程序调试面板的职责 调试面板位于 [map.wxml](D:/dev/cmr-mini/miniprogram/pages/map/map.wxml) 和 [map.ts](D:/dev/cmr-mini/miniprogram/pages/map/map.ts)。 它的职责是: - 查看当前局状态 - 查看 GPS / 心率 / 罗盘状态 - 切换真实源 / 模拟源 - 触发局部调试动作 - 展示设备列表与连接状态 - 辅助验证 HUD 和地图渲染状态 调试面板的原则是: - 只操作已经存在的系统能力 - 不直接篡改规则内部状态 - 调试动作尽量对应单一层 也就是说: - 连接心率带,属于 sensor 调试 - 切换玩法,属于 rule 调试 - 切 mock bridge,属于传感输入源调试 - 心率分区测试,属于 telemetry 调试 ### 18.3 外部模拟器的职责 外部模拟器位于 [tools/mock-gps-sim](D:/dev/cmr-mini/tools/mock-gps-sim)。 它的职责是: - 室内模拟 GPS 位置流 - 模拟路径回放 - 模拟心率值和心率曲线 - 复用真实 `game.json / KML / tiles` - 在电脑侧快速联调地图、HUD、规则 当前它已经支持: - 载入 `game.json` - 载入 KML 控制点 - 载入瓦片模板 - 实时 GPS 发送 - 路径编辑 - 路径回放 - 导入轨迹文件 - 固定心率发送 - 分区样本心率发送 - 真实样本心率模板发送 ### 18.4 当前 mock 通信协议 GPS: ```json { "type": "mock_gps", "timestamp": 1711267200000, "lat": 31.2304, "lon": 121.4737, "accuracyMeters": 6, "speedMps": 2.4, "headingDeg": 135 } ``` 心率: ```json { "type": "mock_heart_rate", "timestamp": 1711267200000, "channelId": "runner-a", "bpm": 148 } ``` 调试日志: ```json { "type": "debug-log", "timestamp": 1711267200000, "channelId": "runner-a", "scope": "gps-logo", "level": "info", "message": "logo ready" } ``` 当前三条链已经拆开: - GPS:`.../mock-gps` - 心率:`.../mock-hr` - 日志:`.../debug-log` 同时三条链统一使用同一个 `channelId` 做最小隔离: - 模拟器顶部设置一个全局“模拟通道号” - 小程序调试面板也设置同一个“模拟通道号” - 只有 `channelId` 精确匹配的数据才会被消费 ### 18.5 当前推荐的联调顺序 如果要联调一个完整玩法,建议按这个顺序: 1. 先载入配置与控制点 2. 小程序切到 mock GPS / mock heart rate 3. 外部模拟器连接桥接 4. 先用固定值测试最小闭环 5. 再切路径回放和心率样本 6. 最后再上真机 + 真实 GPS / BLE 心率带验收 这个顺序的好处是: - 先验证链路 - 再验证动态过程 - 最后再验证真实设备行为 ### 18.6 为什么调试体系很重要 对这类 GPS / 心率 / 定向玩法类项目来说,最大的开发瓶颈往往不是代码本身,而是: - 需要空间移动 - 需要真实设备 - 需要时间成本 - 需要复现复杂路径 因此调试与模拟体系本身就是底座能力的一部分,而不是临时工具。 后续建议继续把以下能力都优先放在这条体系里: - 轨迹回放模板 - 心率样本模板 - 特殊玩法样本 - 多阶段规则验证入口 - 关键状态可视化 --- 本文档用于当前阶段开发总结。 后续如果架构继续升级,建议直接在本文件上持续迭代,而不是另起多份架构说明,避免信息分散。 --- ## 19. 当前开发约定 为了避免后续开发过程中重新把边界做乱,当前阶段建议固定以下约定。 ### 19.1 修改代码前先判断归属层 新增需求时,先判断它属于哪一层,再落代码: - 地图能力问题:优先进 `engine` - 玩法判定问题:优先进 `game/rules` - 通用运动信息问题:优先进 `game/telemetry` - 纯展示文案或 HUD 结构问题:优先进 `game/presentation` 或页面层 - 声音、震动、地图脉冲等反馈问题:优先进 `game/feedback` - 调试动作与仿真工具问题:优先进调试面板或外部模拟器 如果一个需求同时改动太多层,通常说明边界还没想清楚。 ### 19.2 先扩事件,再扩功能 当前架构是事件驱动主链,因此新增能力时优先顺序建议为: 1. 先定义输入事件 2. 再定义规则输出的 `effect` 或 `presentation` 3. 最后补具体的 UI / 声音 / 动效消费端 不要直接在页面里写死业务副作用,也不要让渲染器自己猜业务状态。 ### 19.3 build 版本号约定 当前小程序页面 build 号统一写在: - [map.ts](D:/dev/cmr-mini/miniprogram/pages/map/map.ts) 约定为: - 每次发生用户可感知的页面 / 地图 / 调试 / 玩法改动时,自增 1 - 只做文档或纯注释修改时,可以不变 这样方便现场确认当前安装包和工作副本是否一致。 ### 19.4 提交约定 当前开发分支约定使用: - `codex/*` 提交时建议遵守: - 一次提交只围绕一个相对完整的目标 - 不把开发工具噪声配置混进业务提交 - 优先提交可运行的阶段闭环,而不是半成品 当前项目里一个典型例子是: - [project.config.json](D:/dev/cmr-mini/project.config.json) 这类本地开发工具配置,不应该默认混入功能提交。 ### 19.5 自测约定 当前阶段每次改动后的最低自测要求建议是: - 代码改动后执行 `npm run typecheck` - 外部模拟器脚本改动后,额外执行 `node --check` - 涉及地图 / HUD / 层级表现的问题,以真机为准 - 涉及 BLE / GPS / 后台行为的问题,必须至少走一遍真机联调 原因是: - 微信开发工具对 `webgl canvas`、原生层级、蓝牙、后台音频的模拟都不完全可靠 ### 19.6 当前阶段已知现实约束 这些不是 bug,但开发时要心里有数: - 微信开发工具里,`webgl canvas` 与普通视图层级表现可能和真机不一致 - 后台音频能力当前不稳定,因此已经回退为前台策略 - BLE 心率带连接存在底层资源释放延迟,重连逻辑必须保守处理 - 模拟器与真机联调时,公网 / 局域网 WebSocket 地址要明确区分 ### 19.7 当前阶段最重要的判断标准 如果新增一个功能后出现以下现象,就要停下来重新审视设计: - 为了做一个玩法,不得不大改 `MapEngine` - 为了做一个 HUD 文案,不得不改 `RuleEngine` - 为了做一个声音效果,不得不改地图渲染逻辑 - 为了做一个模拟输入,不得不绕过传感层直接写 telemetry 出现这些情况,通常说明实现绕过了当前架构,应优先回到分层原则重新整理。 --- ## 20. 后续演进路线图 当前架构已经从“能跑”进入“可持续扩展”阶段。 后续建议不要无序加功能,而是按时间层次推进。 ### 20.1 近期目标 近期目标的重点不是再造新架构,而是继续把现在这套底座打磨稳。 建议优先推进: - 继续完善顺序赛与积分赛的细节体验 - 继续扩充调试面板,但保持分组清晰 - 继续扩充外部模拟器,让常见流程都能室内复现 - 持续清理地图引擎与规则层之间可能重新耦合的点 - 对关键链路补更多真机验证经验 这个阶段的目标是: - 日常开发大部分功能都能在室内完成联调 - 新玩法加入时不需要再反复返工底层分层 ### 20.2 中期目标 中期更适合开始扩充玩法族,而不是继续只做当前两种玩法。 建议方向: - 增加更多 `RulePlugin` - 开始让 `modeState` 真正承载玩法私有状态 - 继续把玩法专属 HUD / 地图表现做成独立 adapter - 把玩法配置进一步从“少量字段”扩成更完整的 `game` 段 - 让反馈系统支持更清晰的事件 profile 这个阶段的核心判断标准是: - 新玩法主要新增规则与 presentation 文件 - 而不是大改已有底层模块 ### 20.3 远期目标 远期不建议先追求花哨表现,而是继续把底座能力做成可复用资产。 可能的方向包括: - 玩法模板化 - 外部模拟器支持更多传感器类型 - 赛后回放与事件日志分析 - 多人 / 团队玩法 - 更完整的内容配置系统 - 更稳定的后台能力方案 到这个阶段,理想状态是: - 这套底座不只服务当前项目 - 也能被后续其它运动地图类 App 直接复用 ### 20.4 当前最值得持续投入的底座能力 如果要挑几项最值得长期投入的底座能力,当前建议是: - `RulePlugin` 扩展能力 - `Telemetry` 的稳定性和通用性 - 外部模拟器 - 调试面板 - 地图路线符号系统 - 反馈事件体系 这些能力的共同价值在于: - 一次投入,可以被多个玩法复用 ### 20.5 当前不建议过早投入的方向 以下方向并不是不做,而是不建议当前阶段优先投入: - 为了开发工具显示偏差而重构正式页面层级 - 一上来做复杂多人同步 - 一上来做过重的后台音频方案 - 在还没有足够玩法前就过度抽象成庞大平台 当前更重要的是: - 把已经证明有效的主链打磨稳 - 再逐步扩展玩法和能力 # 21. 今日新增宿主层约定 今天这批调整的重点,不是新增某一个玩法,而是继续把宿主层的同步与展示链路收干净。它们的价值在于: - 不只服务顺序赛或积分赛 - 后续新玩法也能直接复用 - 让“用户操作”和“程序状态变化”最终都汇到同一条同步链 ## 21.1 配置入口元信息开始进入宿主快照 远程配置入口目前已经不再只服务地图与路线加载,也开始承载活动级元信息。 当前 [remoteMapConfig.ts](/D:/dev/cmr-mini/miniprogram/utils/remoteMapConfig.ts) 已经能从配置中解析并标准化这些字段: - `app.id` - `app.title` - `schemaVersion` - `version` - `map.*` - `playfield.*` - `game.*` 其中: - `configAppId` - `configSchemaVersion` - `configVersion` 已经进入 [MapEngine](/D:/dev/cmr-mini/miniprogram/engine/map/mapEngine.ts) 的宿主状态,并可以被上层面板消费。 这意味着配置入口开始具备“双重职责”: - 一方面装配运行所需的地图、路线、玩法参数 - 另一方面提供活动本身的识别信息和版本信息 这一步为后面配置文件完全成为游戏入口打下了基础。 ## 21.2 统一状态提交管线继续强化 前面已经建立了: - `event -> RulePlugin -> GameResult` 今天继续强化的是: - `GameResult -> MapEngine.commitGameResult(...) -> 页面 / telemetry / feedback / renderer` 这条宿主管线的目标是: - 不再靠某个功能里“顺手刷新一下状态” - 而是让所有玩法动作最终都走统一提交 当前这条统一提交链已经被用于: - 开始对局 - 打点 - 跳点 - 积分赛 focus 选点 - GPS 更新后的规则推进 - 切换配置后的定义加载 这一步的意义非常大,因为游戏过程中真正需要同步的状态越来越多,例如: - 当前目标 - 已完成 / 已跳过状态 - HUD 文案 - 打点与跳点按钮可用性 - guidance 音效状态 - 地图高亮 - telemetry 目标距离 - renderer 表现层 只要这些更新仍然能统一收敛到 `commitGameResult(...)` 这条链上,架构就还是健康的。 ## 21.3 游戏信息面板成为新的宿主诊断出口 11 号按钮现在不再是临时调试入口,而是一个正式的“游戏信息面板”。 当前它由 [map.ts](/D:/dev/cmr-mini/miniprogram/pages/map/map.ts) 负责开关,由 [MapEngine](/D:/dev/cmr-mini/miniprogram/engine/map/mapEngine.ts) 提供快照数据,页面层只负责展示。 面板当前分成两部分: - `Local` - `Global` 其中: - `Local` 负责展示本地已知的实时状态 - `Global` 目前还是占位,后面联网后再接全局赛事态 `Local` 当前已经可以展示: - 比赛名称 - 配置版本 - Schema 版本 - 活动 ID - 当前玩法 - 当前状态 - 当前目标 - 进度 - 分数 - 打点与跳点规则 - GPS、心率、里程、速度、卡路里等本地信息 这块的设计原则是: - 页面不自己拼业务逻辑 - 引擎只提供统一快照 - 后面全局数据接入时,继续沿这套面板结构扩展 ## 21.4 侧边按钮体系正式分成两类 今天还把地图页的侧边按钮体系收了一版,避免后面按钮越来越多后状态逻辑变乱。 当前已经明确分成两类: ### A. 三态功能按钮 适用于: - `4 exit` - `11 info` - `16 skip` 它们统一使用三种状态: - `muted` - `default` - `active` 这些按钮的视觉态不再由模板零散判断,而是由页面层统一派生。 当前 [map.ts](/D:/dev/cmr-mini/miniprogram/pages/map/map.ts) 中已经有: - `SideActionButtonState` - `buildSideButtonState(...)` 输入的是主状态,例如: - `showGameInfoPanel` - `skipButtonEnabled` - `gameSessionStatus` 输出的是最终按钮态,例如: - `sideButton4Class` - `sideButton11Class` - `sideButton16Class` 这意味着: - 有些状态虽然是用户点击触发的 - 有些状态虽然是程序运行中更新的 - 但最后按钮显示都统一走同一套派生逻辑 ### B. 循环模式按钮 左上角那个按钮不属于三态功能按钮,而是单独的“模式切换器”。 当前它根据 `sideButtonMode` 循环切换不同图标: - `btn_more1` - `btn_more2` - `btn_more3` 它的本质是: - 点击后切换显示模式 - 图标跟随当前模式变化 而不是普通意义上的启用/禁用/激活按钮 把这类按钮和普通功能按钮分开,是为了后面继续扩展侧边栏时不把状态语义搅乱。 ## 21.5 当前阶段的判断 到今天这一步,可以比较明确地说: - 统一提交管线已经有雏形 - 游戏信息面板已经成为宿主状态对外展示窗口 - 侧边按钮体系已经开始统一状态派生 这三件事都不是只服务某个玩法的补丁,而是在继续把“通用宿主层”做稳。 当前最重要的不是继续为了理论纯度大拆,而是: - 先用这套底座承接后续配置字段和玩法细化 - 一旦再次出现“某类状态总是漏同步”的真实问题,再继续沿统一提交链收口 --- ## 22. 平台能力边界补充 最近这轮 H5 与传感器排查,已经明确了一件事: - 当前项目最初使用的是**个人主体**小程序 - 这会直接影响部分平台能力 目前已经确认受影响或可能受影响的能力包括: - `web-view` - `Compass` - `Accelerometer` - 其它部分设备能力在 `iOS / Android` 上的稳定性 这意味着: - 某些问题并不一定是代码实现错误 - 也可能是主体能力边界导致 例如当前已经确认: - 配置文件可以正常读取,不代表同域名 H5 页面就一定能在 `web-view` 中打开 - 某些传感器在个人主体环境下表现不稳定,不代表原生链路本身一定有问题 因此当前阶段建议: - 继续优先开发原生主流程 - H5 与高级传感器按“预留 + 待验证”处理 - 待企业主体审核通过后,再统一做专项回归 详细说明见: - [平台能力说明.md](D:/dev/cmr-mini/doc/debug/平台能力说明.md) --- ## 23. 内容体验与 H5 分工定案 这一阶段又把“原生内容”和 “H5 定制内容”的边界试清楚了。 ### 23.1 已确认的边界 在企业主体环境下: - `web-view` 已经可以正常打开 - 但它不适合作为“原生弹窗里的局部 H5 内容区” - 真机上更接近整页原生容器 因此当前正式定案为: - **即时内容弹窗:原生** - **详情页 / 互动任务页:H5** - **结果页:原生兜底 + H5 全屏增强** ### 23.2 当前已经落地的内容体验链 现在控制点内容已经不是单一文本弹层,而是: - 原生内容卡模板 - `minimal` - `story` - `focus` - 配置驱动的展示控制 - `title` - `body` - `clickTitle` - `clickBody` - `autoPopup` - `once` - `priority` - 原生内容卡 CTA - `查看详情` 当前行为是: - 打点或点击后先显示原生内容卡 - 如果该内容配置了 H5 详情,则卡片中显示 `查看详情` - 点击后再进入 H5 详情页 - H5 失败时继续回退原生内容 ### 23.3 这一步的意义 这一步非常关键,因为它把过去“内容到底原生还是 H5”的混乱边界收清楚了: - 地图过程中的节奏控制,交给原生 - 深度内容和强互动,交给 H5 - 原生永远保底 后面继续扩展: - 拍照上传 - 语音留言 - 小游戏 - 定制结果页 都会沿这条边界继续推进,而不是重新混在一个弹层里。