为什么值得单独做成系统
弹珠台小游戏里,球撞上霓虹蘑菇加 100 分,连续点亮三盏灯后倍率翻倍,进入上方轨道会触发多球。玩家能接受夸张物理,但不能接受分数乱跳或挡板没有响应。
弹珠台的乐趣来自高速碰撞和密集反馈。Phaser 可以用 Matter 或 Arcade 做基础物理,但得分、连击、冷却、防重复触发和球状态必须独立于碰撞回调,否则同一次碰撞可能在一帧内加多次分。 本文会按一个可上线的小系统来拆,不追求炫技,而是把数据结构、状态流、玩家反馈、调试工具和发布检查说清楚。Phaser 的优势是让画面和交互快速成型,但越是快速,越需要把规则层和表现层分开。
核心架构
flowchart TD
N1["BallBody"] --> N2["BumperSensor"]
N2["BumperSensor"] --> N3["CooldownGate"]
N3["CooldownGate"] --> N4["ScoreEvent"]
N4["ScoreEvent"] --> N5["ComboMeter"]
N5["ComboMeter"] --> N6["TableFeedback"]
N4["ScoreEvent"] --> N7["MultiballState"]
N6["TableFeedback"] --> N8["Phaser Effects"]
这张图的重点是单向流动。BallBody、BumperSensor、ScoreEvent、ComboMeter、CooldownGate、MultiballState、TableFeedback 不应该互相随意读写。输入或场景事件进入模型,模型输出快照或事件,Phaser 表现层再根据结果更新 Sprite、Graphics、Sound 和 UI。只要这条边界稳定,后续加内容、加难度、加存档或加多人同步,都不会把系统推倒重写。
碰撞点要变成得分事件
不要在 Matter collisionstart 里直接加分。碰撞回调先生成 ScoreEvent,记录球 id、目标 id、碰撞速度、时间和类型。CooldownGate 再判断是否有效。这样高速球贴着 bumper 抖动时,不会一帧内刷出大量分数。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
挡板手感比真实物理重要
弹珠台挡板需要立即响应。按键后可以短时间内给挡板角速度或直接设置目标角度,再让物理跟随。释放时快速回弹。真实铰链参数不一定好玩,关键是按下瞬间要有力量,球被击中时要有明确速度变化。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
连击倍率要有节奏
ComboMeter 可以按命中类型累积,命中高价值目标延长计时,普通目标只保底。倍率衰减要可见,比如计时环逐渐缩短。玩家需要知道自己还有多久能维持连击。不要让倍率隐藏在后台,弹珠台玩法很依赖即时反馈。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
防重复触发要按目标配置
不同目标需要不同冷却。蘑菇 bumper 可能 120ms 内只算一次,轨道入口需要直到球离开才重置,灯组开关只在状态变化时得分。CooldownGate 应按 targetId 和 targetType 维护,而不是全局冷却。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
多球状态要保护性能
多球很爽,但多个球同时触发碰撞会让特效、音效和分数弹窗暴增。MultiballState 可以限制同帧音效数量、合并分数弹窗、降低非主球拖尾。否则多球阶段可能不是刺激,而是卡顿。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
失球判定要有缓冲
球掉到底部并不一定立刻结束。可以给一个 drain sensor,球进入后禁用输入、播放掉落动画,然后结算。若开启救球保护,短时间内自动弹回。救球保护必须显示给玩家,不然会像判定错误。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
调试要记录最近碰撞
开发面板显示最近 20 次 ScoreEvent:时间、球、目标、速度、是否被冷却挡掉、得分。弹珠台 bug 往往是某个目标得分太多或完全不得分,有事件列表才能快速定位。
实现时建议先用最简单的调试图形验证规则,再接正式美术。比如先画出区域、方向、时间轴、占用格或检测范围,确认数据正确后再添加粒子、镜头、音效和过渡动画。这样做不花哨,但能避免很多“看起来对,规则其实错”的问题。
TypeScript 实现骨架
interface ScoreEvent { ballId: string; targetId: string; targetType: string; speed: number; at: number }
class CooldownGate {
private last = new Map<string, number>();
constructor(private defaults: Record<string, number>) {}
accept(event: ScoreEvent) {
const key = `${event.ballId}:${event.targetId}`;
const cooldown = this.defaults[event.targetType] ?? 100;
const prev = this.last.get(key) ?? -Infinity;
if (event.at - prev < cooldown) return false;
this.last.set(key, event.at);
return true;
}
}
class ComboMeter {
multiplier = 1;
expiresAt = 0;
hit(now: number, value: number) {
if (now > this.expiresAt) this.multiplier = 1;
this.multiplier = Math.min(8, this.multiplier + value);
this.expiresAt = now + 3500;
}
}
这段代码只展示核心边界,不是完整项目代码。真实项目里还需要补配置加载、错误码、事件派发、对象池、性能采样和测试。关键是让核心规则能独立运行,Phaser 层只是把规则结果变成玩家能感知的反馈。
落地步骤
- 第一,先把 BallBody 和 BumperSensor 写成普通 TypeScript 模型。不要让它们依赖 Phaser Scene、Sprite 或 Camera。核心模型越普通,越容易写测试、做编辑器预览和复现玩家问题。
- 第二,Phaser 层只做适配:接收输入、播放动画、更新图形、触发音效。它可以很薄,但必须清楚。只要某段规则开始读取 Sprite 的 visible、alpha 或动画状态,就说明边界正在变脏。
- 第三,给 ScoreEvent 或同等复杂的中间结果做调试显示。开发模式里能看到状态、阈值、候选对象、失败原因和耗时,后续调参才不会靠猜。
- 第四,准备三组测试夹具:正常流程、边界流程、错误配置。正常流程验证体验,边界流程验证稳定性,错误配置验证系统会报出人能看懂的问题。
检查清单
- 确认 BallBody 的状态可以序列化,能写入存档或调试日志。
- 确认 BumperSensor 的配置有默认值、版本号和校验错误。
- 确认快速点击、暂停、切后台、读档和切场景不会重复提交关键事件。
- 确认失败反馈足够具体,玩家能知道是条件不足、输入中断、资源不够还是规则禁止。
- 确认低端机有降级策略,尤其是粒子、音效、动态对象和调试图层。
- 确认开发模式可以导出最近关键事件,方便复现玩家反馈。
常见误区
第一类误区,是把表现当成事实。动画播完、按钮亮着、Sprite 存在,都只能说明表现层当前长什么样,不能说明规则已经完成。规则事实应该存在于模型和事件里。
第二类误区,是只为了第一个关卡写逻辑。第一个关卡对象少、输入慢、节奏简单,临时判断很难暴露问题。等内容增加,重复触发、配置错误和性能峰值会一起出现,早期的边界会决定后期成本。
第三类误区,是没有设计失败路径。复杂系统一定会遇到失败,好的失败路径会告诉玩家和开发者发生了什么;坏的失败路径只会留下一句操作失败,甚至什么都不显示。
发布前验证
发布前至少跑一次规则级测试和一次运行时冒烟。规则级测试不需要启动浏览器,直接喂数据,断言状态和事件。运行时冒烟则在 Phaser 场景里验证输入、反馈、暂停、重开和边界情况。若系统涉及经济、存档或排行榜,还要记录 requestId 或事件 id,保证重复提交不会造成重复奖励或重复扣费。
额外实践建议
- 碰撞回调只产事件,不直接改分数。
- 每个目标都要有冷却策略和调试名字。
- 多球阶段要限制音效和弹窗,避免反馈变成噪声。
运行时观测与调参
弹珠台要记录的不是总分,而是得分来源。每局结束后可以输出命中次数最多的目标、被冷却挡掉的事件数、最高倍率持续时间、掉球位置和多球阶段平均帧耗时。若某个 bumper 贡献了异常高分,可能是冷却太短,也可能是物理形状让球卡在边缘反复触发。若玩家经常在同一个 drain 入口掉球,可能是挡板角度、回弹力度或侧边坡度有问题。调参时要同时看手感和经济:分数太慷慨会让倍率失去意义,分数太吝啬又会让碰撞反馈显得空。把 ScoreEvent 流留住,弹珠台才有持续打磨的基础。
物理参数也要成组保存。球的 restitution、friction、挡板角速度、bumper 冲量和桌面重力共同决定手感,单独改一个数很容易破坏其他区域。建议为“教学桌”“高速桌”“多球桌”分别保存预设,并在调试面板里实时切换。每次切换预设时记录分数分布和掉球时间,才能判断手感变化是否真的让玩法更好,而不是只让球看起来更快。
还要给每张桌子保留安全重开入口。若球因为物理误差卡在装饰缝里,系统应在几秒低速后轻微弹出或重置到最近发射轨道,并记录 stuck 事件。玩家可以接受偶尔救球,不能接受一局因为卡住被迫刷新页面。
结语
弹珠台玩法:物理碰撞、连击倍率和判分保护 的关键不是某个 API,而是把可解释的规则交给模型,把可感知的反馈交给 Phaser。只要这条线清楚,项目就能持续扩展;如果所有逻辑都塞进 Scene 回调,第一版越快,后面的维护压力越大。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。