最近除了折腾抖音聊天机器人,我还做了一个小黑盒工坊小程序,名字叫 《盒盒大作战》。

玩法很容易理解:从一颗小球开始,吃地图上的食物长大,再吃掉比自己小的对手。想主动进攻可以分身,想和场上的绿刺互动可以吐孢子。局外用对局获得的盒币抽皮肤,再回去开下一局。

听起来是个不复杂的休闲游戏,但真正做起来,花时间最多的往往是那些不显眼的部分:手机画面怎么横过来,手指坐标怎么对应,排行榜昵称为什么没更新,分身为什么弹不远,还有被吃掉以后究竟要重开还是复活。

这篇以当前开发版本的代码为准,记录已经做进项目的内容和一些取舍,不把设计文档中的计划当成已经上线的功能。

一、先做单机对局,再接云端排行榜

这个项目目前是玩家与 AI 的单机对局。场上的其他球由本地规则控制,不是真人匹配;云端排行榜负责玩家之间的成绩比较,也不等于已经做了实时联机。

一开始我想要的节奏是碎片时间玩一两局,目标是几分钟内就有明显的成长、进攻和被追赶体验。不过,这不是严格到点就结束的游戏:实现里还有十分钟软时限的继续选择,玩家也能在被吃后选择复活。

核心循环分成两层:

  • 局内:吃食物、长大、分身进攻、绕开危险,争取更高体积和击杀。
  • 局外:结算盒币、收集皮肤和孢子外观、查看纪录,再开一局。

先把这两层做完整,比一开始同时堆联机、房间和匹配更方便观察玩法。当前也没有把联机写成已经完成的能力。

二、Canvas 画游戏,DOM 做菜单

技术上用了 JavaScript、Vite 和 Canvas 2D,平台接入通过 @heybox/hb-sdk,项目当前安装的 SDK 版本是 0.8.4。

我把主要代码分开了:

1
2
3
4
5
src/game/       世界状态、移动、碰撞、AI、复活等逻辑
src/render/ 相机、球体、皮肤、粒子和小地图绘制
src/input/ 触屏摇杆、操作按钮与键鼠输入
src/platform/ 平台资料、存档、榜单和分享封装
src/ui/ 主页、商店、排行榜和结算界面

逻辑层尽量不依赖页面元素,因此可以在没有浏览器的情况下模拟一段对局,检查质量变化、分身、吞噬和复活。菜单和榜单则用 DOM,方便排版和点击。

模拟使用固定的 30 Hz 步长,渲染在两次逻辑状态间做插值。这样不用让移动速度跟着屏幕刷新率变化,也减少了逻辑更新和画面刷新节奏不一致时的抖动。

HUD 也没有每帧重建,而是降低更新频率。时间、盒币和排行榜数字不需要每一帧都重新生成页面,在手机 WebView 里省掉这部分工作更实际。

三、成长和分身的手感,比规则说明难调

“大球吃小球”只有一句话,但真正影响操作的是很多连续变化的数值:体型越大移动越慢,分身会前冲,母球会受到后坐力,分出去以后还需要等待合体。

这次调过两处很明显的体验问题。

第一处是小球视野太近。开局时周围看不远,很难提前发现对手,所以给小球阶段加了随舞台高度变化的视野约束,成长过程中再逐渐拉远。新一局首帧也直接使用正确镜头,避免开始后才突然缩放。

第二处是分身弹射距离不够。只提高基础初速还不够,后期大球会很快碰到原有速度上限,所以又把分身爆发和普通移动的速度上限分开,同时降低母球反向冲量,让进攻距离增加时不至于把母球推得太远。

这些调整有隔离模拟作比较,也有对应的测试脚本。但数值通过检查,只能说明行为符合预期;手感是否舒服,还要放到真机上操作。

四、让 AI 有不同的打法

这里的 AI 是游戏规则和策略判断,没有调用大语言模型,也不会每次移动都产生模型 Token 开销。

普通对手分成偏休闲、普通和猎手等类型,组合会参考已有对局的隐藏统计调整。它们并不是完全相同的一群球:有些主要在场上进食,有些会形成更直接的追赶威胁。

最近还加了一种红名困难 AI,叫 “刺术师”。它不只是更积极地追着玩家跑,还会尝试从绿刺后方吐孢子推刺、养刺、借刺掩护撤离,或者摆诱饵后藏起来,等目标接近再分身扑出。

每局只保留一个刺术师名额,它被吃后以小球重新出生。它的行动仍然经过移动、吐球、分身和吞噬逻辑,推刺也要支付实际质量成本,而不是直接把攻击结果写进游戏状态。

需要说明的是,现有普通 AI 和猎手有辅助成长曲线,用来控制单机对局节奏。这套难度设计并不等于所有对手与玩家拥有完全相同的成长条件;“没有凭空执行攻击”与“没有辅助成长”是两个不同的问题。

五、皮肤扭蛋:抽到的就从池子里移走

局外商店做了五台扭蛋机:盒娘小表情、大头照 1.0、大头照 2.0,以及两组 CUBE 孢子外观。球体皮肤和吐出的孢子外观分别装备,地图自然生成的食物仍然是彩色圆点。

我选择了一个比较直接的抽取规则:只从这台机器尚未拥有的外观里均匀抽取,已经拥有的不会再出。

核心逻辑可以概括成:

1
2
3
const remaining = pool.filter(item => !owned.includes(item.id));
const item = remaining[Math.floor(Math.random() * remaining.length)];
owned.push(item.id);

实际实现还要检查盒币、处理免费教学抽取、保存结果,以及在集齐后禁用按钮。抽到新外观先进入收藏,玩家在结果弹窗里选择收下或立即装备,不会强制替换当前装扮。

这个规则也意味着,同一台机器有多少款,成功抽取多少次就能集齐。旧设计文档里仍有一张按“允许重复抽取”估算收集成本的表,它与当前代码不一致,不能拿来说明现在的抽取概率。

外观贴图在加载失败或尚未完成时有绘制兜底,避免整个球直接消失。盒币是项目内通过对局获得的游戏币;本文不把它与小黑盒账户里的平台货币混为一谈。

六、横屏适配不是旋转一下 CSS 就结束

这个游戏更适合手机横握,但宿主是否能切到原生横屏,要看客户端是否支持对应能力。官方文档也将窗口方向控制标为实验能力,布局仍应依据实际窗口和安全区适配。窗口方向与全屏文档

项目优先请求宿主横屏,不支持时把整个游戏舞台旋转 90 度来兜底。

画面旋转以后,至少还有三件事要一起处理:

  1. 舞台宽高要交换,Canvas 的绘制尺寸也要对应。
  2. 触摸和鼠标坐标要做逆变换,否则手指往右,球可能往另一个方向走。
  3. 刘海、系统栏等安全区要映射到舞台坐标,不能直接照搬竖屏页面的边距。

所以项目里统一维护舞台尺寸和坐标转换,菜单、摇杆和画布都按同一套方向状态工作。全屏成功也不能假定系统手势条一定消失,重要按钮仍要留在安全区域里。

七、分享截图,又遇到了横竖屏问题

结算和排行榜都有分享入口。我希望分享的是当前成绩画面,但在部分 Android 宿主里,原生横屏截图曾出现裁剪越界问题。

目前的处理是在截图期间临时恢复竖屏窗口,把横版游戏画面按比例放进这个窗口,等待宿主与页面尺寸稳定,再计算合法的截图区域。完成后恢复布局;方向恢复失败时,仍保留 CSS 横屏兜底。

这一步不能只取一次尺寸就立刻截图。切换方向后,宿主窗口、页面布局和 Canvas 重绘不一定同时完成,过早计算的裁剪框可能已经过期。

分享操作也做了进行中状态保护,避免连续点击启动多条截图流程。无法截图时则尝试退回文字分享入口。平台分享会进入用户操作流程,不代表小程序可以静默替用户发社区帖子。官方社区分享说明

八、排行榜和登录,要分清身份与展示资料

项目有最高体积、击杀和最快霸屏三类榜单。云端不可用或以游客体验时,先保留本地纪录;用户主动绑定本次游戏身份后,再认领游客成绩并尝试同步个人最佳成绩。

这里有两个容易混淆的概念。

平台可以返回用户资料,不代表游戏已经建立了自己的登录会话。 当前实现要求用户在本次运行中主动点击登录游戏,再绑定有效身份;这个状态放在内存,退出后不会自动恢复。项目没有因此建立开发者服务端会话或保存服务器登录 Token。平台的资料读取和服务端授权码登录本来也是不同路径。官方身份与登录说明

另一个问题是榜单上的昵称、头像。它们放在成绩记录的展示信息里,但同分或更差的成绩提交不会覆盖已有的分数和展示信息。接口返回成功,并不表示这次提交的新昵称已经保存。官方榜单提交规则

因此当前页面可以用新读取的资料更新自己的显示,其他玩家看到的旧云端成绩仍可能带着旧资料,直到有更优成绩更新。项目没有通过伪造更高分或删掉旧成绩来强行刷新昵称,因为那会改变真实成绩和排序。

身份归并也不能靠昵称,同名、改名都很常见。当前实现用平台提供的小程序内身份标识关联纪录;已经属于一个账号的成绩,不会因为切换账号就复制给另一个账号。

九、被吃后复活,保留的是同一局

最近增加了对局内复活:玩家被吃掉后暂停当前对局,有七秒选择立即复活或者结束。没有选择就自动结算,切到后台再回来时也按实际截止时间判断。

复活次数目前不限,但只重建玩家的小球和出生状态。地图、AI 的体型和位置、已有分身、局时,以及本局累计成绩和收益都继续保留,最终结束才结算一次。

如果复活时直接重建整个世界,就会变成重新开局;如果每次死亡都结算奖励,又会出现一局重复发放。因此复活被单独做成状态流程,而不是在“再来一局”按钮上改个名字。

无限复活也改变了成绩的含义:它记录的是这局累计表现,不是一次生命的生存表现。后续如果增加不同模式或调整排行榜规则,还需要重新考虑这一点,目前不把它描述成已经解决了所有竞技公平性问题。

十、项目能构建,和玩家能顺利玩完一局之间还有距离

这套代码有无头玩法检查,以及分身调参、困难 AI、复活、身份、榜单分页和截图流程等测试脚本。它们方便固定条件复现问题,也避免修一个功能时破坏另一个功能。

不过,模拟和浏览器预览不能覆盖所有宿主行为。登录层曾经在 DOM 中存在,却被更高层级的主页盖住;横屏在浏览器里正常,到了手机宿主又可能采用另一种窗口尺寸;截图调用能开始,也不意味着分享出来的图一定完整。

我会把代码检查、页面布局和真机体验当成不同层面的验证。当前文章介绍的是本地开发版本的功能,不以配置里声明的平台列表来承诺所有设备都已经验收,也不把上传提审当成已经公开上架。

接下来仍需要继续观察对局节奏、成长速度、复活后的收益,以及不同手机上的操作体验。能打开只是第一步,玩家能看清场面、理解按钮,并顺利玩到结算,才算把这个小游戏做得更完整。