Ei kuvausta

Rockz-Home 1a6008449e chore: ignore local auxiliary files 1 päivä sitten
GameConfigSample d1cc6cc473 Add config-driven game host updates 1 viikko sitten
backend 527b4c78a9 推进活动列表第一刀与联调回归 3 päivää sitten
doc e17dc931ba docs: add h5 experience and tracking specs 1 päivä sitten
event 3ef841ecc7 feat: 收敛玩法运行时配置并加入故障恢复 5 päivää sitten
miniprogram 527b4c78a9 推进活动列表第一刀与联调回归 3 päivää sitten
realtime-gateway 6964e26ec9 同步前后端联调与文档更新 4 päivää sitten
tmp 6964e26ec9 同步前后端联调与文档更新 4 päivää sitten
tools b09c21c814 完善联调标准化与诊断链路 3 päivää sitten
typings 129ea935db 完善活动运营域与联调标准化 3 päivää sitten
.gitattributes 5fea805ac3 chore: harden repository defaults 2 viikkoa sitten
.gitignore 1a6008449e chore: ignore local auxiliary files 1 päivä sitten
b2f.md 527b4c78a9 推进活动列表第一刀与联调回归 3 päivää sitten
b2t.md 527b4c78a9 推进活动列表第一刀与联调回归 3 päivää sitten
f2b.md 527b4c78a9 推进活动列表第一刀与联调回归 3 päivää sitten
f2t.md 527b4c78a9 推进活动列表第一刀与联调回归 3 päivää sitten
package-lock.json 2cf0bb76b4 Add mock GPS simulator and configurable location sources 1 viikko sitten
package.json 3ef841ecc7 feat: 收敛玩法运行时配置并加入故障恢复 5 päivää sitten
project.config.json 175a16001e chore: 提交调试文档与模拟器改动 5 päivää sitten
publish-event-config.ps1 3ef841ecc7 feat: 收敛玩法运行时配置并加入故障恢复 5 päivää sitten
readme-develop.md 527b4c78a9 推进活动列表第一刀与联调回归 3 päivää sitten
readme.md 6964e26ec9 同步前后端联调与文档更新 4 päivää sitten
t2b.md 527b4c78a9 推进活动列表第一刀与联调回归 3 päivää sitten
t2f.md 527b4c78a9 推进活动列表第一刀与联调回归 3 päivää sitten
todolist.md 6964e26ec9 同步前后端联调与文档更新 4 päivää sitten
tsconfig.json 03abe28d8c feat: initialize mini program map engine 2 viikkoa sitten
tsconfig.runtime-smoke.json 3ef841ecc7 feat: 收敛玩法运行时配置并加入故障恢复 5 päivää sitten

readme-develop.md

CMR Mini 开发架构阶段总结

文档版本:v1.20 最后更新:2026-04-03 19:26:23

文档维护约定:

  • 仓库内 Markdown 文档统一在标题下方标注 文档版本最后更新
  • 最后更新 必须写到日期时间,例如 2026-04-02 08:28:05
  • 后续新建文档或更新文档内容时,必须同步更新这两项元信息。

本文档用于记录当前阶段小程序的整体架构、分层原则、事件驱动链路、模拟器体系,以及后续继续扩展时应遵守的边界。

当前补充约定:

  • 多线程联调场景下,正式架构与长期结论优先沉淀到 doc/
  • 当前联调架构的阶段总结见:
  • 活动列表最小产品方案见:
  • 面向后端线程的阶段性实施说明,优先写入根目录 t2b.md
  • backend 新增写给总控线程的回写板:
  • 面向前端线程的阶段性实施说明,优先写入根目录 t2f.md
  • frontend 写给总控线程的回写板:
  • 分层原则固定为:
    • 玩家用前端
    • 管理者用后端
    • 中间层负责契约、架构、性能、健壮性与伸缩性
    • 不把后台复杂性直接暴露给玩家界面
  • 后台生产闭环的正式架构稿见:
  • 正式上线时的数据库与服务发布流程见:
  • 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

2.2 游戏相关目录

位于 miniprogram/game

2.3 地图引擎目录

位于 miniprogram/engine

  • map:宿主编排与地图主入口
  • renderer:WebGL/2D 渲染
  • sensor:GPS、心率、罗盘、模拟源
  • tile:瓦片缓存与加载
  • camera:相机与坐标换算
  • overlay:地图叠加层相关
  • layer:图层定义

2.4 外部模拟器目录

位于 tools/mock-gps-sim


3. 设计原则

3.1 地图引擎只负责地图能力

地图引擎负责:

  • 地图缩放、平移、旋转
  • 真实 GPS 与模拟 GPS 的接入
  • 真实心率带与模拟心率的接入编排
  • 路线、控制点、轨迹、编号的绘制
  • WebGL 与 2D 文本层的渲染同步
  • 把规则层产出的 map presentation 转成地图显示

地图引擎不负责:

  • 当前玩法规则判定
  • 计分
  • 完成条件
  • 打点策略
  • 游戏胜负

核心入口文件是 mapEngine.ts

3.2 规则层只负责玩法状态推进

规则层只关心:

  • 游戏事件输入
  • 当前 session state
  • 规则推进后的 next state
  • 产出展示所需的 presentation
  • 产出反馈所需的 effects

当前已实现两种玩法:

  • classic-sequential
  • score-o

对应规则文件:

3.3 通用运行信息独立为 telemetry

Telemetry 层不属于具体玩法,也不属于地图引擎。

它负责:

  • 用时
  • 距离
  • 当前速度
  • 平均速度
  • 当前目标距离
  • 心率
  • 卡路里
  • 精度
  • HUD 的通用体能颜色分级

入口文件:

3.4 展示状态独立为 presentation

当前展示层已经拆成两块:

  • map presentation
  • hud presentation

这样规则层可以分别决定:

  • 地图该怎么画
  • HUD 该显示什么

而不让渲染器自己猜玩法语义。

文件:

3.5 反馈统一走 effect 消费

声音、震动、UI 动效不是直接写在规则里,也不是直接写在页面里,而是由规则层产出 GameEffect[],再交给反馈层消费。

反馈层入口:

目前已经挂入:

  • 声音层
  • 震动层
  • 页面动效层
  • 地图瞬时特效层

4. 运行主链路

当前整套系统的主链路如下:

远程配置 / KML / 静态内容
-> GameDefinition
-> 传感输入 (GPS / 心率 / 罗盘 / 模拟源)
-> MapEngine 编排
-> GameRuntime / RulePlugin
-> GameSessionState + Presentation + Effects
-> Renderer / HUD / FeedbackDirector

更细一点可以拆成两条并行链。

4.1 模块关系图

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 玩法链

地图点击 / GPS 更新 / 打点按钮
-> GameEvent
-> GameRuntime.dispatch()
-> RulePlugin.reduce()
-> nextState + presentation + effects

4.3 信息链

GPS / 心率 / session 状态
-> TelemetryEvent
-> TelemetryRuntime
-> TelemetryState
-> TelemetryPresentation
-> HUD / 颜色 / 卡路里 / 距离

5. 核心模块说明

5.1 MapEngine

文件:

职责:

  • 宿主编排器
  • 不直接写玩法判断
  • 管理地图视图状态
  • 编排 LocationController
  • 编排 HeartRateInputController
  • 编排 CompassHeadingController
  • 编排 GameRuntime
  • 编排 TelemetryRuntime
  • 编排 FeedbackDirector
  • 汇总成 MapEngineViewState 透传给页面

这里是全局协调中心,但不是业务大杂烩。

应继续坚持:

  • 新玩法不要往这里塞规则逻辑
  • 新传感器/模拟器可以继续从这里编排接入

5.2 GameRuntime

文件:

职责:

  • 载入 GameDefinition
  • 根据 mode 解析具体规则插件
  • 对外提供统一 dispatch(event)
  • 维护:
    • state
    • presentation
    • mapPresentation
    • hudPresentation

当前支持:

  • 顺序打点规则
  • 积分赛规则

这是后续继续加玩法的入口。

5.3 TelemetryRuntime

文件:

职责:

  • 消费 gps_updated
  • 消费 heart_rate_updated
  • 同步 session 状态
  • 计算:
    • elapsed
    • mileage
    • distanceToTarget
    • speed
    • averageSpeed
    • calories
    • heart rate tone

当前心率 / 速度逻辑:

  • 有心率时,颜色和卡路里优先走心率逻辑
  • 无心率时,自动回落到速度代理区间

5.4 FeedbackDirector

文件:

职责:

  • 消费 GameEffect[]
  • 分发到:
    • SoundDirector
    • HapticsDirector
    • UiEffectDirector

当前后台音频方案已经暂时回退成前台-only。
原因是小程序后台音频 loop 行为不稳定,当前阶段不再强行实现。

5.5 LocationController

文件:

职责:

  • 管理真实 GPS 与模拟 GPS 双源
  • 暴露统一的位置更新
  • 管理 mock bridge URL、连接状态、调试状态

当前支持:

  • real
  • mock

并且 mock GPS 已经可以通过外部模拟器驱动。

5.6 HeartRateInputController

文件:

这是最近新增的一层,职责是统一真实心率带与模拟心率源。

它内部编排:

  • 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 模拟

消息协议:

{
  "type": "mock_gps",
  "timestamp": 1711267200000,
  "channelId": "runner-a",
  "lat": 31.2304,
  "lon": 121.4737,
  "accuracyMeters": 6,
  "speedMps": 2.4,
  "headingDeg": 135
}

9.3 心率模拟

消息协议:

{
  "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 或页面里塞逻辑。

flowchart TD
  A["新增玩法需求"] --> B["确定规则目标<br/>完成条件 / 得分 / 失败条件 / 解锁逻辑"]
  B --> C["定义 modeState<br/>只放玩法私有状态"]
  C --> D["新增 RulePlugin<br/>reduce + buildPresentation"]
  D --> E["补充 GameDefinition 配置项<br/>让远程配置可描述此玩法"]
  E --> F["拆 presentation<br/>map / hud 分别表达"]
  F --> G{"是否需要新通用统计?"}
  G -- "是" --> H["扩 TelemetryRuntime<br/>仅加入跨玩法可复用信息"]
  G -- "否" --> I["保持 telemetry 不动"]
  H --> J{"是否需要新反馈?"}
  I --> J
  J -- "是" --> K["新增 GameEffect 消费端<br/>声音 / 震动 / UI / 地图特效"]
  J -- "否" --> L["保持 feedback 不动"]
  K --> M["调试面板补入口<br/>只做该层对应的调试能力"]
  L --> M
  M --> N["外部模拟器补样本<br/>仅在确有联调价值时添加"]
  N --> O["真机联调与验收"]

16.1 新玩法落地时的边界检查

新增玩法前,建议先问下面几个问题:

  1. 这是规则状态,还是通用信息?

    • 规则状态放 modeState
    • 通用信息放 telemetry
  2. 这是地图显示语义,还是 HUD 文案语义?

    • 地图相关放 map presentation
    • HUD 相关放 hud presentation
  3. 这是玩法逻辑,还是地图能力?

    • 玩法逻辑放 rule plugin
    • 地图能力放 engine / renderer
  4. 这是单玩法专属能力,还是跨玩法复用能力?

    • 专属能力优先局部实现
    • 复用能力再提升为全局层

16.2 建议的新增玩法最小模板

后续新增一个玩法时,建议至少补这些文件或模块:

  • game/rules/<newMode>Rule.ts
  • game/content/courseToGameDefinition.ts 中对应模式的定义装配
  • game/core/gameDefinition.ts 中新增 mode 支持
  • game/presentation/* 中新增该玩法需要的 map/hud 字段
  • pages/map/map.wxml 只在确有显示需求时接新 HUD 文案
  • 调试面板里补最小测试入口

如果一个新玩法一上来就需要大改:

  • MapEngine
  • TelemetryRuntime
  • Renderer

那通常说明这次设计边界还没想清楚,应该先回头重审玩法抽象。


17. 关键文件索引

地图与宿主

规则层

Presentation

Telemetry

Feedback

传感层

外部模拟器


18. 调试与模拟体系

当前项目已经不再依赖“必须出门跑一遍”才能测通主流程。
围绕调试和联调,已经形成了两条互补链路:

  • 小程序内调试面板
  • 外部模拟器

二者是互补关系,不是替代关系。

18.1 调试体系关系图

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.wxmlmap.ts

它的职责是:

  • 查看当前局状态
  • 查看 GPS / 心率 / 罗盘状态
  • 切换真实源 / 模拟源
  • 触发局部调试动作
  • 展示设备列表与连接状态
  • 辅助验证 HUD 和地图渲染状态

调试面板的原则是:

  • 只操作已经存在的系统能力
  • 不直接篡改规则内部状态
  • 调试动作尽量对应单一层

也就是说:

  • 连接心率带,属于 sensor 调试
  • 切换玩法,属于 rule 调试
  • 切 mock bridge,属于传感输入源调试
  • 心率分区测试,属于 telemetry 调试

18.3 外部模拟器的职责

外部模拟器位于 tools/mock-gps-sim

它的职责是:

  • 室内模拟 GPS 位置流
  • 模拟路径回放
  • 模拟心率值和心率曲线
  • 复用真实 game.json / KML / tiles
  • 在电脑侧快速联调地图、HUD、规则

当前它已经支持:

  • 载入 game.json
  • 载入 KML 控制点
  • 载入瓦片模板
  • 实时 GPS 发送
  • 路径编辑
  • 路径回放
  • 导入轨迹文件
  • 固定心率发送
  • 分区样本心率发送
  • 真实样本心率模板发送

18.4 当前 mock 通信协议

GPS:

{
  "type": "mock_gps",
  "timestamp": 1711267200000,
  "lat": 31.2304,
  "lon": 121.4737,
  "accuracyMeters": 6,
  "speedMps": 2.4,
  "headingDeg": 135
}

心率:

{
  "type": "mock_heart_rate",
  "timestamp": 1711267200000,
  "channelId": "runner-a",
  "bpm": 148
}

调试日志:

{
  "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. 再定义规则输出的 effectpresentation
  3. 最后补具体的 UI / 声音 / 动效消费端

不要直接在页面里写死业务副作用,也不要让渲染器自己猜业务状态。

19.3 build 版本号约定

当前小程序页面 build 号统一写在:

约定为:

  • 每次发生用户可感知的页面 / 地图 / 调试 / 玩法改动时,自增 1
  • 只做文档或纯注释修改时,可以不变

这样方便现场确认当前安装包和工作副本是否一致。

19.4 提交约定

当前开发分支约定使用:

  • codex/*

提交时建议遵守:

  • 一次提交只围绕一个相对完整的目标
  • 不把开发工具噪声配置混进业务提交
  • 优先提交可运行的阶段闭环,而不是半成品

当前项目里一个典型例子是:

这类本地开发工具配置,不应该默认混入功能提交。

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 已经能从配置中解析并标准化这些字段:

  • app.id
  • app.title
  • schemaVersion
  • version
  • map.*
  • playfield.*
  • game.*

其中:

  • configAppId
  • configSchemaVersion
  • configVersion

已经进入 MapEngine 的宿主状态,并可以被上层面板消费。

这意味着配置入口开始具备“双重职责”:

  • 一方面装配运行所需的地图、路线、玩法参数
  • 另一方面提供活动本身的识别信息和版本信息

这一步为后面配置文件完全成为游戏入口打下了基础。

21.2 统一状态提交管线继续强化

前面已经建立了:

  • event -> RulePlugin -> GameResult

今天继续强化的是:

  • GameResult -> MapEngine.commitGameResult(...) -> 页面 / telemetry / feedback / renderer

这条宿主管线的目标是:

  • 不再靠某个功能里“顺手刷新一下状态”
  • 而是让所有玩法动作最终都走统一提交

当前这条统一提交链已经被用于:

  • 开始对局
  • 打点
  • 跳点
  • 积分赛 focus 选点
  • GPS 更新后的规则推进
  • 切换配置后的定义加载

这一步的意义非常大,因为游戏过程中真正需要同步的状态越来越多,例如:

  • 当前目标
  • 已完成 / 已跳过状态
  • HUD 文案
  • 打点与跳点按钮可用性
  • guidance 音效状态
  • 地图高亮
  • telemetry 目标距离
  • renderer 表现层

只要这些更新仍然能统一收敛到 commitGameResult(...) 这条链上,架构就还是健康的。

21.3 游戏信息面板成为新的宿主诊断出口

11 号按钮现在不再是临时调试入口,而是一个正式的“游戏信息面板”。

当前它由 map.ts 负责开关,由 MapEngine 提供快照数据,页面层只负责展示。

面板当前分成两部分:

  • Local
  • Global

其中:

  • Local 负责展示本地已知的实时状态
  • Global 目前还是占位,后面联网后再接全局赛事态

Local 当前已经可以展示:

  • 比赛名称
  • 配置版本
  • Schema 版本
  • 活动 ID
  • 当前玩法
  • 当前状态
  • 当前目标
  • 进度
  • 分数
  • 打点与跳点规则
  • GPS、心率、里程、速度、卡路里等本地信息

这块的设计原则是:

  • 页面不自己拼业务逻辑
  • 引擎只提供统一快照
  • 后面全局数据接入时,继续沿这套面板结构扩展

21.4 侧边按钮体系正式分成两类

今天还把地图页的侧边按钮体系收了一版,避免后面按钮越来越多后状态逻辑变乱。

当前已经明确分成两类:

A. 三态功能按钮

适用于:

  • 4 exit
  • 11 info
  • 16 skip

它们统一使用三种状态:

  • muted
  • default
  • active

这些按钮的视觉态不再由模板零散判断,而是由页面层统一派生。

当前 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 与高级传感器按“预留 + 待验证”处理
  • 待企业主体审核通过后,再统一做专项回归

详细说明见:


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
  • 原生永远保底

后面继续扩展:

  • 拍照上传
  • 语音留言
  • 小游戏
  • 定制结果页

都会沿这条边界继续推进,而不是重新混在一个弹层里。