|
|
1 hari lalu | |
|---|---|---|
| GameConfigSample | 1 Minggu lalu | |
| backend | 3 hari lalu | |
| doc | 1 hari lalu | |
| event | 5 hari lalu | |
| miniprogram | 3 hari lalu | |
| realtime-gateway | 4 hari lalu | |
| tmp | 4 hari lalu | |
| tools | 3 hari lalu | |
| typings | 3 hari lalu | |
| .gitattributes | 2 minggu lalu | |
| .gitignore | 1 hari lalu | |
| b2f.md | 3 hari lalu | |
| b2t.md | 3 hari lalu | |
| f2b.md | 3 hari lalu | |
| f2t.md | 3 hari lalu | |
| package-lock.json | 1 Minggu lalu | |
| package.json | 5 hari lalu | |
| project.config.json | 5 hari lalu | |
| publish-event-config.ps1 | 5 hari lalu | |
| readme-develop.md | 3 hari lalu | |
| readme.md | 4 hari lalu | |
| t2b.md | 3 hari lalu | |
| t2f.md | 3 hari lalu | |
| todolist.md | 4 hari lalu | |
| tsconfig.json | 2 minggu lalu | |
| tsconfig.runtime-smoke.json | 5 hari lalu |
文档版本:v1.20 最后更新:2026-04-03 19:26:23
文档维护约定:
文档版本 和 最后更新。最后更新 必须写到日期时间,例如 2026-04-02 08:28:05。本文档用于记录当前阶段小程序的整体架构、分层原则、事件驱动链路、模拟器体系,以及后续继续扩展时应遵守的边界。
当前补充约定:
doc/。EventPresentation 统一导入入口Event 默认 active 三元组固化GET /events/{eventPublicID} 透出 currentPresentation / currentContentBundleGET /events/{eventPublicID}/play 透出 currentPresentation / currentContentBundlelaunch 透出 presentation / contentBundlepresentationId / contentBundleIdrelease detail 已统一活动运营摘要ContentBundle 统一导入入口第一版已完成Bootstrap Demo 已可补齐:
place / map asset / tile release / course source / course set / course variant / runtime binding一键补齐 Runtime 并发布 已可从空白状态跑完整测试链一键标准回归 与 回归结果汇总 已接入 workbench当前 Launch 实际配置摘要 已接入 workbench前端调试日志 已接入 workbenchevt_demo_001evt_demo_score_o_001evt_demo_variant_manual_001content manifestpresentation schema活动文案样例回归结果汇总当前 Launch 实际配置摘要前端调试日志当前阶段的核心目标已经从“把地图画出来”升级为“建立一套可长期扩展的运动地图游戏底座”。
这套底座已经具备以下关键能力:
当前工程可以概括为 6 层:
此外,还有一条独立的开发调试链:
tools/mock-gps-sim这两者不参与业务规则,只作为开发辅助工具。
位于 miniprogram:
位于 miniprogram/game:
地图引擎负责:
地图引擎不负责:
核心入口文件是 mapEngine.ts。
规则层只关心:
当前已实现两种玩法:
classic-sequentialscore-o对应规则文件:
Telemetry 层不属于具体玩法,也不属于地图引擎。
它负责:
入口文件:
当前展示层已经拆成两块:
map presentationhud presentation这样规则层可以分别决定:
而不让渲染器自己猜玩法语义。
文件:
声音、震动、UI 动效不是直接写在规则里,也不是直接写在页面里,而是由规则层产出 GameEffect[],再交给反馈层消费。
反馈层入口:
目前已经挂入:
当前整套系统的主链路如下:
远程配置 / KML / 静态内容
-> GameDefinition
-> 传感输入 (GPS / 心率 / 罗盘 / 模拟源)
-> MapEngine 编排
-> GameRuntime / RulePlugin
-> GameSessionState + Presentation + Effects
-> Renderer / HUD / FeedbackDirector
更细一点可以拆成两条并行链。
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
地图点击 / GPS 更新 / 打点按钮
-> GameEvent
-> GameRuntime.dispatch()
-> RulePlugin.reduce()
-> nextState + presentation + effects
GPS / 心率 / session 状态
-> TelemetryEvent
-> TelemetryRuntime
-> TelemetryState
-> TelemetryPresentation
-> HUD / 颜色 / 卡路里 / 距离
文件:
职责:
LocationControllerHeartRateInputControllerCompassHeadingControllerGameRuntimeTelemetryRuntimeFeedbackDirectorMapEngineViewState 透传给页面这里是全局协调中心,但不是业务大杂烩。
应继续坚持:
文件:
职责:
GameDefinitionmode 解析具体规则插件dispatch(event)statepresentationmapPresentationhudPresentation当前支持:
这是后续继续加玩法的入口。
文件:
职责:
gps_updatedheart_rate_updated当前心率 / 速度逻辑:
文件:
职责:
GameEffect[]SoundDirectorHapticsDirectorUiEffectDirector当前后台音频方案已经暂时回退成前台-only。
原因是小程序后台音频 loop 行为不稳定,当前阶段不再强行实现。
文件:
职责:
当前支持:
realmock并且 mock GPS 已经可以通过外部模拟器驱动。
文件:
这是最近新增的一层,职责是统一真实心率带与模拟心率源。
它内部编排:
HeartRateController:真实 BLE 心率带MockHeartRateBridge:模拟心率桥对上游暴露统一接口:
当前支持:
realmock这样 telemetry 和 HUD 不需要知道心率是从 BLE 来的还是模拟器来的。
核心特征:
地图语义:
核心特征:
地图语义:
当前规则层已经预留并开始使用 modeState。
含义:
GameSessionState 只放跨玩法共用字段modeState这为后续新增玩法提供了稳定扩展入口。
目前 HUD 有两屏:
HUD 当前颜色由 telemetry 驱动。
规则:
当前 6 档对应:
当前心率带链路已经相对完整。
当前不是“多设备同时连接”,而是:
原因:
已实现:
这一段实现过多轮修正,目前逻辑重点是:
这是目前心率带重连稳定的关键。
解决“GPS / 心率类 App 每次都要出去跑才能测”的问题。
当前外部模拟器已经支持:
game.json消息协议:
{
"type": "mock_gps",
"timestamp": 1711267200000,
"channelId": "runner-a",
"lat": 31.2304,
"lon": 121.4737,
"accuracyMeters": 6,
"speedMps": 2.4,
"headingDeg": 135
}
消息协议:
{
"type": "mock_heart_rate",
"timestamp": 1711267200000,
"bpm": 148
}
外部模拟器当前支持:
真实样本模式又细分成:
当前已经调整为:
这样更适合长面板配置、多人联调隔离和过程日志观察,不会让地图区跟着滚动。
当前系统已经较明确地进入了事件驱动模型。
规则层的输入是 GameEvent。
典型事件:
session_startedgps_updatedpunch_requestedcontrol_focusedsession_ended规则层输出:
nextStatepresentationeffectsTelemetry 层的输入是 TelemetryEvent。
典型事件:
session_state_updatedtarget_updatedgps_updatedheart_rate_updated反馈层消费的是 GameEffect[]。
这样:
都可以走统一 effect 通道,而不是各处散写。
这是当前项目后续扩展最重要的边界之一。
适合放:
这些是通用运行信息。
适合放:
这些会影响规则推进。
适合放:
这些只是为了显示。
后面开发新增字段时,必须先判断它属于哪层,避免再次耦合。
当前架构已经通过以下场景验证过方向正确:
这说明当前架构不是只能跑一个 Demo,而是已经具备继续扩展的基础。
目前已经确认:
webgl canvas + 普通 view 上的层级模拟并不稳定因此后续验收原则是:
已经尝试过后台 guidance 音频方案,但当前阶段决定先回退:
后续如果重新开启,需要:
心率带重连问题已经证明:
所以后续涉及 BLE 时,必须继续保留:
到目前为止,这个项目已经完成了从“功能堆叠”到“可扩展结构”的第一阶段转变。
现阶段最重要的成果不是某一个按钮或某一个玩法,而是以下架构能力已经成立:
这意味着后续继续开发时:
而不应该再把所有逻辑堆回 MapEngine 或页面里。
当前阶段之后,建议按以下优先级推进:
modeState 专属状态但无论怎么扩,建议始终遵守本文档里的边界原则。
后续新增玩法时,建议始终按下面这条路径落地,而不是直接往 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["真机联调与验收"]
新增玩法前,建议先问下面几个问题:
这是规则状态,还是通用信息?
modeStatetelemetry这是地图显示语义,还是 HUD 文案语义?
map presentationhud presentation这是玩法逻辑,还是地图能力?
这是单玩法专属能力,还是跨玩法复用能力?
后续新增一个玩法时,建议至少补这些文件或模块:
game/rules/<newMode>Rule.tsgame/content/courseToGameDefinition.ts 中对应模式的定义装配game/core/gameDefinition.ts 中新增 mode 支持game/presentation/* 中新增该玩法需要的 map/hud 字段pages/map/map.wxml 只在确有显示需求时接新 HUD 文案如果一个新玩法一上来就需要大改:
MapEngineTelemetryRuntimeRenderer那通常说明这次设计边界还没想清楚,应该先回头重审玩法抽象。
当前项目已经不再依赖“必须出门跑一遍”才能测通主流程。
围绕调试和联调,已经形成了两条互补链路:
二者是互补关系,不是替代关系。
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
它的职责是:
调试面板的原则是:
也就是说:
外部模拟器位于 tools/mock-gps-sim。
它的职责是:
game.json / KML / tiles当前它已经支持:
game.jsonGPS:
{
"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"
}
当前三条链已经拆开:
.../mock-gps.../mock-hr.../debug-log同时三条链统一使用同一个 channelId 做最小隔离:
channelId 精确匹配的数据才会被消费如果要联调一个完整玩法,建议按这个顺序:
这个顺序的好处是:
对这类 GPS / 心率 / 定向玩法类项目来说,最大的开发瓶颈往往不是代码本身,而是:
因此调试与模拟体系本身就是底座能力的一部分,而不是临时工具。
后续建议继续把以下能力都优先放在这条体系里:
本文档用于当前阶段开发总结。
后续如果架构继续升级,建议直接在本文件上持续迭代,而不是另起多份架构说明,避免信息分散。
为了避免后续开发过程中重新把边界做乱,当前阶段建议固定以下约定。
新增需求时,先判断它属于哪一层,再落代码:
enginegame/rulesgame/telemetrygame/presentation 或页面层game/feedback如果一个需求同时改动太多层,通常说明边界还没想清楚。
当前架构是事件驱动主链,因此新增能力时优先顺序建议为:
effect 或 presentation不要直接在页面里写死业务副作用,也不要让渲染器自己猜业务状态。
当前小程序页面 build 号统一写在:
约定为:
这样方便现场确认当前安装包和工作副本是否一致。
当前开发分支约定使用:
codex/*提交时建议遵守:
当前项目里一个典型例子是:
这类本地开发工具配置,不应该默认混入功能提交。
当前阶段每次改动后的最低自测要求建议是:
npm run typechecknode --check原因是:
webgl canvas、原生层级、蓝牙、后台音频的模拟都不完全可靠这些不是 bug,但开发时要心里有数:
webgl canvas 与普通视图层级表现可能和真机不一致如果新增一个功能后出现以下现象,就要停下来重新审视设计:
MapEngineRuleEngine出现这些情况,通常说明实现绕过了当前架构,应优先回到分层原则重新整理。
当前架构已经从“能跑”进入“可持续扩展”阶段。
后续建议不要无序加功能,而是按时间层次推进。
近期目标的重点不是再造新架构,而是继续把现在这套底座打磨稳。
建议优先推进:
这个阶段的目标是:
中期更适合开始扩充玩法族,而不是继续只做当前两种玩法。
建议方向:
RulePluginmodeState 真正承载玩法私有状态game 段这个阶段的核心判断标准是:
远期不建议先追求花哨表现,而是继续把底座能力做成可复用资产。
可能的方向包括:
到这个阶段,理想状态是:
如果要挑几项最值得长期投入的底座能力,当前建议是:
RulePlugin 扩展能力Telemetry 的稳定性和通用性这些能力的共同价值在于:
以下方向并不是不做,而是不建议当前阶段优先投入:
当前更重要的是:
今天这批调整的重点,不是新增某一个玩法,而是继续把宿主层的同步与展示链路收干净。它们的价值在于:
远程配置入口目前已经不再只服务地图与路线加载,也开始承载活动级元信息。
当前 remoteMapConfig.ts 已经能从配置中解析并标准化这些字段:
app.idapp.titleschemaVersionversionmap.*playfield.*game.*其中:
configAppIdconfigSchemaVersionconfigVersion已经进入 MapEngine 的宿主状态,并可以被上层面板消费。
这意味着配置入口开始具备“双重职责”:
这一步为后面配置文件完全成为游戏入口打下了基础。
前面已经建立了:
event -> RulePlugin -> GameResult今天继续强化的是:
GameResult -> MapEngine.commitGameResult(...) -> 页面 / telemetry / feedback / renderer这条宿主管线的目标是:
当前这条统一提交链已经被用于:
这一步的意义非常大,因为游戏过程中真正需要同步的状态越来越多,例如:
只要这些更新仍然能统一收敛到 commitGameResult(...) 这条链上,架构就还是健康的。
11 号按钮现在不再是临时调试入口,而是一个正式的“游戏信息面板”。
当前它由 map.ts 负责开关,由 MapEngine 提供快照数据,页面层只负责展示。
面板当前分成两部分:
LocalGlobal其中:
Local 负责展示本地已知的实时状态Global 目前还是占位,后面联网后再接全局赛事态Local 当前已经可以展示:
这块的设计原则是:
今天还把地图页的侧边按钮体系收了一版,避免后面按钮越来越多后状态逻辑变乱。
当前已经明确分成两类:
适用于:
4 exit11 info16 skip它们统一使用三种状态:
muteddefaultactive这些按钮的视觉态不再由模板零散判断,而是由页面层统一派生。
当前 map.ts 中已经有:
SideActionButtonStatebuildSideButtonState(...)输入的是主状态,例如:
showGameInfoPanelskipButtonEnabledgameSessionStatus输出的是最终按钮态,例如:
sideButton4ClasssideButton11ClasssideButton16Class这意味着:
左上角那个按钮不属于三态功能按钮,而是单独的“模式切换器”。
当前它根据 sideButtonMode 循环切换不同图标:
btn_more1btn_more2btn_more3它的本质是:
而不是普通意义上的启用/禁用/激活按钮
把这类按钮和普通功能按钮分开,是为了后面继续扩展侧边栏时不把状态语义搅乱。
到今天这一步,可以比较明确地说:
这三件事都不是只服务某个玩法的补丁,而是在继续把“通用宿主层”做稳。
当前最重要的不是继续为了理论纯度大拆,而是:
最近这轮 H5 与传感器排查,已经明确了一件事:
目前已经确认受影响或可能受影响的能力包括:
web-viewCompassAccelerometeriOS / Android 上的稳定性这意味着:
例如当前已经确认:
web-view 中打开因此当前阶段建议:
详细说明见:
这一阶段又把“原生内容”和 “H5 定制内容”的边界试清楚了。
在企业主体环境下:
web-view 已经可以正常打开因此当前正式定案为:
现在控制点内容已经不是单一文本弹层,而是:
minimalstoryfocustitlebodyclickTitleclickBodyautoPopuponcepriority查看详情当前行为是:
查看详情这一步非常关键,因为它把过去“内容到底原生还是 H5”的混乱边界收清楚了:
后面继续扩展:
都会沿这条边界继续推进,而不是重新混在一个弹层里。