Brak opisu

zhangyan 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
GameConfigSample d1cc6cc473 Add config-driven game host updates 1 tydzień temu
backend 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
doc 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
event 3ef841ecc7 feat: 收敛玩法运行时配置并加入故障恢复 6 dni temu
miniprogram 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
realtime-gateway 6964e26ec9 同步前后端联调与文档更新 5 dni temu
tmp 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
tools b09c21c814 完善联调标准化与诊断链路 4 dni temu
typings 129ea935db 完善活动运营域与联调标准化 4 dni temu
.gitattributes 5fea805ac3 chore: harden repository defaults 2 tygodni temu
.gitignore 1a6008449e chore: ignore local auxiliary files 2 dni temu
b2b.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
b2f.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
b2t.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
f2b.archive.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
f2b.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
f2f.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
f2t.archive.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
f2t.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
package-lock.json 2cf0bb76b4 Add mock GPS simulator and configurable location sources 2 tygodni temu
package.json 3ef841ecc7 feat: 收敛玩法运行时配置并加入故障恢复 6 dni temu
project.config.json 175a16001e chore: 提交调试文档与模拟器改动 6 dni temu
publish-event-config.ps1 3ef841ecc7 feat: 收敛玩法运行时配置并加入故障恢复 6 dni temu
readme-develop.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
readme.md 6964e26ec9 同步前后端联调与文档更新 5 dni temu
t2b.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
t2f.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
t2w.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
todolist.md 6964e26ec9 同步前后端联调与文档更新 5 dni temu
tsconfig.json 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu
tsconfig.runtime-smoke.json 3ef841ecc7 feat: 收敛玩法运行时配置并加入故障恢复 6 dni temu
w2t.md 6cd16f08dd 推进活动系统最小成品闭环与游客体验 12 godzin temu

readme-develop.md

CMR Mini 开发架构阶段总结

文档版本:v1.24 最后更新:2026-04-07 22:35:00

文档维护约定:

  • 仓库内 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 当前分工:
      • 维护活动卡片列表第一刀所需最小摘要字段稳定
      • 响应列表页联调中暴露的字段、默认值和语义问题
      • 保持列表页与活动详情页摘要口径一致
      • 收口活动配置、发布、默认活动与自定义活动统一流
      • 第一阶段活动模型按:
      • 单地图
      • 单路线组
      • 单玩法 收口推进
      • 当前不把复杂多地图 / 多路线组 / 多玩法语义硬塞进单个活动对象
      • 为下周运维后台第一期准备对象与接口边界
      • 继续保证三条标准 demo 活动无残留 ongoing session
      • 继续保证一键回归链可从空白环境重复跑通
  • 前端线程建议正式上场时机:
    • 现在已完成活动运营域摘要接线第一刀
    • 当前已完成:
    • 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
  • 原生永远保底

后面继续扩展:

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

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