ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Stack-chan 迷你游戏集:用 compose.mjs 将两款 Piu 游戏合成进一个 mini-app 归档

Stack-chan 迷你游戏集:用 compose.mjs 将两款 Piu 游戏合成进一个 mini-app 归档
  • 嵌入式
  • 物联网
  • 智能硬件
  • 机器人
  • 硬件开发
  • 前端
  • AI 应用

【免费下载链接】stack-chan

A JavaScript-driven M5Stack-embedded super-kawaii robot.

项目地址:https://gitcode.com/gh_mirrors/sta/stack-chan
点击查看免费下载

本示例位于 firmware/mods/examples/stackchan_minigames,演示了 Stack-chan 固件上最典型的 mini-app 打包方式:把两个各自独立的游戏正本(mini_app_sample与stackchan_catch)通过compose.mjs合成进一个 import 白名单兼容的miniapp.ts,最终以一个 archive 向迷你应用启动器注册两款游戏。读完本文,你将掌握 mini-app 外部归档的格式约束、合成脚本的底层原理、manifest.json的资源打包规则,以及从合成、格式化、构建到测试的完整命令链路。

一、示例定位:一个 archive、两个游戏、一个 Gallery 包

Stack-chan 的迷你游戏集是一个“打包”型示例:1 个 mini-app archive,向迷你应用列表注册 2 个游戏:

  • スタックチャン JUMP(sample.stackchan-jump):点击屏幕让 Stack-chan 跳跃,躲避不断逼近的障碍物的横版动作游戏;
  • スタックチャン CATCH(sample.stackchan-catch):左右 3 条车道中移动,接住普通道具、避开炸弹的 Game & Watch 风格接物游戏。

该示例的特殊之处在于它并不维护两份游戏代码。文档明确指出,执行的“正本”是已有的两个独立示例 ——mini_app_sample(JUMP 游戏)与stackchan_catch(CATCH 游戏),各游戏的规则与绘制方式以它们各自的 README 为准。stackchan_minigames目录只负责通过compose.mjs把两份源码合成为符合外部 mini-app import 限制的单一miniapp.ts。

在分发层面,这一组合还作为一个整体发布到 MOD Gallery:web/mod-gallery/samples/stackchan-minigames/stackchan-mod.json定义了sample.stackchan-minigames包(version1.0.0,名称「スタックチャン ミニゲーム集」),entrypoints为["miniapp"],targets覆盖m5stackchan-cores3与simulator,随包附带一个已构建的stackchan-minigames.xsa归档。

二、先理解运行环境:mini-app 的画面所有权与 import 白名单

要理解为什么需要compose.mjs做“合成”,需要先了解 Stack-chan 的 mini-app 运行模型(详见 firmware/docs/mini-apps_ja.md):

  • 画面所有权:迷你应用会临时替换普通的脸部画面,但AppBar 始终由宿主拥有,mini-app 只能获得去掉高度 44 px 之后的320 x 196区域;mini-app 根容器进入clip: true的 viewport,普通绘制不会溢出到 AppBar。
  • 界面层次:Application → 迷你应用 viewport(top: 44, clip: true)→ 迷你应用返回的 Piu Container,其上还有宿主拥有的 AppBar、Overlay、Drawer。迷你应用不会嵌套自己的Application。
  • 外部归档格式:外部迷你应用必须从 archive 的miniapp模块default export 一个定义数组(类型为MiniAppDefinition,包含id、title、create())。外部 archive只能 importminiapp、piu/MC、piu/Timeline,且piu/MC只开放除Application之外的绘制 API。
  • 包含边界(containment):宿主用 Moddable 的Compartment限制外部 mini-app 的 global 与 import,只额外传入读取该 archive 内部资源的archive对象;Modules、网络、设备 I/O、宿主的Application均不交给外部代码。但文档明确警告:这不是针对不可信代码的完整沙箱——因为 Compartment 内创建的原生 Piu 对象会挂进宿主的显示树,Piu 的 constructor/prototype 与宿主共享,当前 API 无法证明不会经父子引用、事件分发或 prototype 修改波及宿主。因此目前只应安装受信任的迷你应用。

正是在这个“import 受限、信任边界明确”的框架下,compose.mjs的职责变得清晰:把两份源码中不符合约束的import语句剥离,并把它们包装为闭包合并到一个模块里,使最终产物只依赖被允许的miniapp/piu/MC。

三、两款游戏的玩法与手感参数

3.1 スタックチャン JUMP:单一 Port 上的跑酷

JUMP 用 Piu 的单个Port绘制整个游戏区域,玩家(32×32 的stack-chan.png精灵)固定横坐标PLAYER_X = 48,只做垂直运动。核心手感参数集中在合成产物 firmware/mods/examples/stackchan_minigames/miniapp.ts 顶部:

参数值含义
FRAME_INTERVAL_MS40帧间隔,即port.interval,约 25 FPS
SPRITE_SIZE32玩家精灵尺寸
GROUND_BOTTOM18地面距 Port 底部的高度,决定#groundY
INITIAL_SPEED/MAX_SPEED4/8障碍物左移初速与上限
JUMP_VELOCITY/GRAVITY-11/1跳跃初速(向上为负)与重力加速度

障碍物有 5 种组合,通过OBSTACLE_HEIGHTS(28/36/44/32/40)、OBSTACLE_WIDTHS(14/18/16/20/14)、OBSTACLE_GAPS(88/120/104/144/112)循环生成;#spawnIndex每生成一个障碍物自增并取模,保证形态循环。

游戏规则与实现细节:

  • 点击(onTouchBegan)触发跳跃,#onGround为 false 时二次点击无效,空中仍保持跳跃状态;碰撞导致#gameOver后,再点一次即#reset重开。
  • 每越过一个障碍物得分 +1,并刷新速度:speed = Math.min(MAX_SPEED, INITIAL_SPEED + (score >> 2)),即每 4 分提速 1,最高 8。
  • 碰撞检测#collides()在玩家包围盒(横向缩进 +6/-5、纵向 +4/-2)与障碍物包围盒之间做 AABB 相交判定,障碍物顶边为groundY - obstacleHeight。
  • 性能取向:onTimeChanged中只对旧/新位置的玩家与障碍物包围盒做局部port.invalidate(),而不是全屏重绘。这是有意为之——文档(mini_app_sample README_ja)说明这是为了“不堆叠大量 Piu 对象,降低嵌入式目标的显示树与重绘负担”。
  • 计分用 5×3 点阵字模绘制(DIGIT_MASKS存 0–9 的 15 位掩码),并以score % 1000显示三位数。

3.2 スタックチャン CATCH:离散状态的三车道接物

CATCH 是一个“离散状态”游戏:落物不连续移动,而是沿 5 个固定台阶(DROP_Y = [31, 50, 69, 88, 107])按 tick 跳变,形成 Game & Watch 式残像。核心状态与规则:

  • 车道与玩家:3 条车道中心LANE_CENTERS = [52, 160, 268],玩家 48×48,movePlayer()把位置钳制在 0–2;触摸左 1/3 左移、右 1/3 右移、中间 1/3 在标题/结束画面用于开始或重试。
  • 道具四类:screw(螺丝)、m5stack、bubble(气泡)为得分道具;bomb为炸弹。接住普通道具 +1 分;漏接(落到本车道没接住)或接到炸弹 +1 miss;miss 累计 3 次进入gameover。避开炸弹不加分。
  • 三档速度:STEP_DELAYS定义了每档在每个台阶的停留 tick ——fast: [1,1,1,1,1]、steady: [1,1,2,1,1]、slow: [2,2,2,2,1];由于各档总时长不同,后发的 fast 落物可能先于先发的 slow 落物着地。
  • 落地间隔约束:MIN_LANDING_GAP_TICKS = 3,canSpawnDrop()会检查新落物落地 tick 与lastLandingTick以及场上每个活动落物的落地 tick 之差,确保任意两次落地之间至少间隔 3 tick,避免出现“无解”局面。
  • 连续补位流:不按“波次”排队,而是场上活动落物 < 2 个时立即用DROP_SEQUENCE(12 项循环序列)补充,保证屏幕上常驻 1–2 个落物。
  • 难度递增:increaseDifficulty()每得 5 分,把tickInterval从初始600 ms下调50 ms,下限250 ms(INITIAL_TICK_INTERVAL_MS=600、MIN_TICK_INTERVAL_MS=250、SPEED_UP_EVERY_SCORE=5、SPEED_UP_STEP_MS=50)。
  • 显示风格:LCD 米色背景#d8deb7,配合浅色幽灵标记#b7be99(车道分隔线、落物轨迹点、非活动态的控制区)与深色活动元素#343a31,刻意用“固定姿势瞬间切换”替代连续动画,重现 Game & Watch 的残像观感。底部LEFT / START / RIGHT控件以浓淡区分当前可用的触摸区。

四、compose.mjs:合成脚本的工作原理

合成脚本 compose.mjs 全流程如下:

node mods/examples/stackchan_minigames/compose.mjs

1. 声明两个正本来源:sources数组通过import.meta.url相对定位到../mini_app_sample/miniapp.ts与../stackchan_catch/miniapp.ts,分别命名为jump与catch。

2. 计算来源哈希:把两个源文件内容以'\0'拼接后做 SHA-256,得到sourceHash,写入生成文件头注释,用于追溯合成产物的来源版本。

3. 模块体改写(moduleBody):

  • 逐行过滤掉以import开头的行——这是满足外部 mini-app import 白名单的关键一步;
  • 把export type|interface|function|class开头的行替换为去掉export的声明;
  • 把export default替换为return(因为整个模块体将被包进 IIFE)。

4. 闭包包装(indent+ 模板拼接):每个模块体统一缩进两格后,包成

const jumpDefinitions = (() => { ... })() const catchDefinitions = (() => { ... })()

5. 合并导出:最后生成

const definitions: readonly MiniAppDefinition[] = Object.freeze([...jumpDefinitions, ...catchDefinitions]) export default definitions

即两个来源各自导出的定义数组被展开合并为一个数组——这正是外部归档要求的miniapp模块 default export 形式。

6.--check模式:若以node compose.mjs --check运行,脚本不会写文件,而是比对现有miniapp.ts与预期输出,不一致即抛错miniapp.ts is stale; run compose.mjs。这一模式被测试链路用来防止合成产物过期(详见第六节)。

五、manifest.json 与资源打包规则

合成后的 manifest.json 结构如下:

{ "include": [ "$(MODDABLE)/examples/manifest_mod.json", "$(MODDABLE)/examples/manifest_typings.json", "../../../host/app/manifest_typings.json" ], "modules": { "miniapp": "./miniapp" }, "resources": { "*": ["./assets/stack-chan"], "*-mask": [ "./assets/player-left", "./assets/player-center", "./assets/player-right", "./assets/screw", "./assets/m5stack", "./assets/bubble", "./assets/bomb", "./assets/miss" ] } }

要点:

  • include沿用了mini_app_sample的 manifest 模式(manifest_mod.json+manifest_typings.json+ 宿主app/manifest_typings.json),提供 TypeScript 能力类型(如MiniAppDefinition)与 Moddable 模块说明符;
  • modules.miniapp指向合成产物./miniapp,这是整个 archive 的唯一代码入口;
  • resources使用了两类键:"*"只收录 JUMP 的stack-chan.png(该资源不带 mask 处理),而"*-mask"收录 CATCH 的 8 张单色 alpha 掩码图(player-left/center/right、screw、m5stack、bubble、bomb、miss),与 CATCH 绘制代码中drawTexture(texture, COLOR_INK, ...)的染色用法一致。

六、完整构建链路:合成 → 格式化 → 构建

原文档给出的命令须在firmware/目录下依次执行:

node mods/examples/stackchan_minigames/compose.mjs npm exec biome format --write mods/examples/stackchan_minigames/miniapp.ts npm run mod:build -- mods/examples/stackchan_minigames/manifest.json --mode=release
  • 第一步:重新合成miniapp.ts(每次修改正本源码后都必须重新执行);
  • 第二步:用项目统一的 biome 格式化器对生成文件做格式化(firmware/下的biome.json定义了项目的格式规则);
  • 第三步:npm run mod:build实际转发到 scripts/firmware.mjs 的mod:build子命令,按manifest.json与--mode=release产出 release 归档。

此外,从firmware/目录也可以直接安装/运行单个 MOD:

npm run mod -- mods/examples/stackchan_minigames/manifest.json

七、自动化验证:合成产物与 Gallery 包的一致性

该示例带有一套完整的测试支撑:

1. 合成产物自身测试——__tests__/definitions/stackchan-minigames.test.js通过manifest.test.json(includemanifest_base、manifest_piu、示例 manifest 与host/modules/testing/manifest.json)加载miniapp模块并断言:

equal(definitions.length, 2, 'one archive should register two mini games') equal(definitions[0].id, 'sample.stackchan-jump', 'the first game should be Stack-chan JUMP') equal(definitions[1].id, 'sample.stackchan-catch', 'the second game should be Stack-chan CATCH')

即一个 archive 恰好注册两个定义,且顺序为 JUMP 在前、CATCH 在后——这与合成脚本中sources数组的顺序严格对应。

2. Gallery 包与固件示例的字节级一致性—— web/mod-gallery/mod-definition.test.mjs 中专门有一条测试“Stack-chan 迷你游戏集 Gallery 包与 Firmware 示例公开相同的运行源码”,逐字节比对web/mod-gallery/samples/stackchan-minigames/miniapp/与firmware/mods/examples/stackchan_minigames/下的 13 个文件(manifest.json、miniapp.ts、两份 README、LICENSE.mouse-follower以及全部 9 张精灵资源),防止 Gallery 分发包与固件示例发生漂移。Gallery 包的 README 进一步说明:stackchan-minigames.xsa面向 XS 17.8.2、以 Moddable SDK 9.5.0 按 M5StackChan CoreS3 配置构建,同一归档也支持 Web 模拟器运行。

八、运行时行为:计时器、退出方式与仅触摸交互

原文档明确了两个值得注意的运行时约定:

  • 计时器生命周期:每个游戏的Port通过 Behavior 的onDisplaying设置port.interval并port.start(),在onUndisplaying中port.stop()——即当 Port 不再显示时,游戏计时器随之停止(JUMP 的StackchanJumpBehavior与 CATCH 的StackchanCatchBehavior均如此实现)。这避免了 mini-app 被切走时后台继续空转。
  • 退出方式:结束游戏使用宿主拥有的 AppBar 左上角“返回”按钮(launcher 与执行画面中该按钮常驻)。外部 mini-app API不暴露设备物理按键能力,因此 JUMP 与 CATCH 都没有绑定 Core2/CoreS3 的实体按钮,操作完全依赖屏幕触摸——这也与 firmware/docs/mini-apps_ja.md 中“当前外部迷你应用只接收触摸事件、不提供宿主按键能力”的说明一致。

九、精灵出典与许可证

原文档明确标注了资源出处:

  • JUMP 的assets/stack-chan.png基于 meganetaaan/mouse-follower 仓库中的 Stack-chan 精灵(commit3258fc6d,packages/mouse-follower/assets/stack-chan.png)制作——原 sprite 的绿色背景被转换为透明,非背景像素未改动(见 mini_app_sample 文档);
  • CATCH 的三种玩家姿势(player-left/center/right.png)以同一图片为设计参考,配合现有道具 sprite sheet 作为风格参考生成,并压缩为单色 alpha 掩码;
  • 分发条件遵循随附的 LICENSE.mouse-follower(MIT License,Copyright (c) meganetaaan)。Gallery 包stackchan-mod.json中的license字段写作"Apache-2.0 AND MIT",对应仓库整体许可与 sprite 来源许可的组合。

十、相关源码与文档导航

主题路径
合成脚本(正本声明、import 剥离、闭包合并、--check)firmware/mods/examples/stackchan_minigames/compose.mjs
合成产物(完整游戏逻辑)firmware/mods/examples/stackchan_minigames/miniapp.ts
归档清单与资源打包firmware/mods/examples/stackchan_minigames/manifest.json
JUMP 正本(含 README_ja、manifest)firmware/mods/examples/mini_app_sample/
CATCH 正本(含玩法细节 README)firmware/mods/examples/stackchan_catch/
mini-app 框架文档(画面所有权、外部归档格式、containment 边界)firmware/docs/mini-apps_ja.md
MOD 与示例总览(mod:build/npm run mod用法)firmware/mods/README.md
Gallery 分发包定义(目标机、artifact、许可证)web/mod-gallery/samples/stackchan-minigames/stackchan-mod.json
一致性回归测试(合成产物断言、Gallery 字节级比对)firmware/mods/examples/stackchan_minigames/tests/definitions/stackchan-minigames.test.js、web/mod-gallery/mod-definition.test.mjs

小结

stackchan_minigames是 Stack-chan 生态中一个“小而完整”的工程范本:它示范了在不维护重复代码的前提下,如何用合成脚本把多个独立 mini-app 正本收敛为一个符合外部归档约束的模块、并以一个 archive 注册多个游戏。对希望开发或打包 Stack-chan mini-app 的开发者来说,本文梳理的合成机制、资源清单规则、构建与回归测试链路,均可直接迁移到自己的项目上。

  • 嵌入式
  • 物联网
  • 智能硬件
  • 机器人
  • 硬件开发
  • 前端
  • AI 应用

【免费下载链接】stack-chan

A JavaScript-driven M5Stack-embedded super-kawaii robot.

项目地址:https://gitcode.com/gh_mirrors/sta/stack-chan
点击查看免费下载

相关推荐

上一篇:彻底解耦DVA状态管理:3个测试陷阱与模型注入实战指南
下一篇:如何快速实现umi与Clerk集成:身份验证与用户管理完整指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进