news 2026/10/15 19:10:30

Egret弹珠游戏源码拆解:从物理系统到碰撞避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Egret弹珠游戏源码拆解:从物理系统到碰撞避坑指南

简介:这是一份基于Egret引擎开发并完整可运行的弹珠游戏源码,面向网页游戏开发者、引擎初学者以及对碰撞检测和物理模拟感兴趣的读者。项目采用TypeScript语言编写,实现了弹珠发射、滚动、碰撞、反弹与得分等核心玩法,代码按模块划分,便于逐步阅读与二次开发。

资源包共收录110个文件,主要包括脚本源码、编译后脚本、图片素材、配置文件、音频文件和少量页面文件,压缩后仅一点零六MB,轻量且结构清晰,适合本地快速加载与调试。

目前已有1286人下载学习,通过逐行分析项目,可掌握游戏场景搭建、对象更新、事件监听、碰撞判断、动画控制和音频触发的完整思路;同时,源码中使用的物理计算与渲染逻辑,也能为后续自研小游戏提供可复用的参考范式。

1. 从零复刻弹珠台:Egret 弹珠游戏源码到底能帮你省多少事

做 H5 小游戏的人,十有八九都动过弹珠台(Pinball)的念头:玩法直观、反馈感强、天然适合计分排行。但真上手才发现,弹珠游戏的“物理味”比普通消除类重得多——“弹珠撞挡板”这种听起来一句话的事,放到 Egret 引擎里做,第一步碰撞体怎么配就能卡住半天。所谓“egret弹珠游戏源码”,核心价值不在那一堆图片和音效,而在把弹珠的碰撞、反弹、下落、计分这套循环用 Egret 的物理系统完整跑通,顺带把触控发射、挡板操作这些交互细节也给你留好了。

这篇笔记的服务对象很明确:想用 Egret(白鹭引擎)做 H5 或小游戏弹珠台、但不想从零踩物理坑的人——无论你是第一次用 Egret 物理系统,还是写过几款休闲游戏想快速出 Demo,都可以按文中的步骤直接抄作业。我们不做大型商业项目的架构设计,只解决“代码拿到手能跑、跑起来能玩、玩坏了知道去哪查”这三件事。文中所有方案基于 Egret 内置物理系统,这也是目前弹珠类源码主流的落地方式:不引第三方库,减少兼容性黑匣子。

2. 弹珠游戏的物理地基:为什么选内置 physics 而不是手写碰撞

2.1 弹珠台的循环逻辑拆解:你真正在写的是什么

弹珠游戏从代码层面看,只有四件事循环往复:发射、飞行、碰撞、计分。发射是给弹珠一个初速度;飞行由重力接管;碰撞是弹珠和挡板、障碍、墙壁交换速度;计分则是对碰撞事件的监听和数值累加。大部分源码的复杂度都不在逻辑本身,而在“碰撞”这一步——Egret 的显示对象默认没有碰撞体积,你要么自己算圆心距离和矩形相交,要么让物理引擎帮你算。

自己手写碰撞在弹珠台上会迅速失控:挡板是旋转体,障碍有圆形有矩形,弹珠本身是高速运动的球体,多物体同时接触时的手写衰减公式很容易让珠子穿墙。常见做法是用 Egret 内置物理系统,它基于 Box2D 封装,碰撞检测、反弹系数、摩擦力都是现成的物理参数;你要做的只是创建 body、配形状、监听 contact。对比下来,手写方案在 3 个障碍物以内还能勉强维持精度,一旦挡板、环形障碍、奖励区同时在场,引擎方案的稳定性会好得多。

Egret 物理系统的工作方式一句话概括:引擎按固定步长迭代,算出每个 body 的位置和角度,再在碰撞回调里通知你的逻辑层。游戏代码不直接改坐标,而是施加力或改变速度,由物理系统推进。这意味着你所有的“动了”都发生在物理世界内部,显示对象只是跟随同步。这个模型搞清楚之后,你读源码里的 addBody、setVelocity、onCollision 这些方法时不会有理解断层——一切都在物理空间运作。

2.2 用 EUI 搭出弹珠台的视觉骨架:场景与容器结构

动物理之前,先把界面层铺好。Egret 项目里常见的做法是用 EUI 组件搭 UI、用普通显示对象画游戏区。弹珠台的特点是游戏区是纵向的、宽度固定,上方是计分面板,下方左右各一个挡板。我一般建议用两个容器:一个 gameLayer 放所有参与物理的显示对象,另一个 uiLayer 放分数、按钮、遮罩。这样物理世界的坐标原点可以对齐 gameLayer,方便后续设置重力方向时做直觉判断。

创建场景骨架的参考代码:

class PinballGame extends egret.DisplayObjectContainer { private gameLayer: egret.DisplayObjectContainer; private uiLayer: egret.DisplayObjectContainer; private physicsWorld: egret.PhysicsSystem; public constructor() { super(); this.initLayers(); this.initPhysics(); } private initLayers(): void { this.gameLayer = new egret.DisplayObjectContainer(); this.uiLayer = new egret.DisplayObjectContainer(); this.addChild(this.gameLayer); this.addChild(this.uiLayer); // gameLayer 负责与物理世界同步,uiLayer 负责纯界面元素 } }

这段代码里有个容易忽略的设计:物理世界初始化时,默认的坐标原点和显示列表的原点未必一致。initLayers 之后必须检查 gameLayer 的位置,如果它在舞台上不是从 0,0 开始,物理 body 的坐标就会出现整体偏移。参数上,gameLayer 建议固定宽高,比如 480×800,这样在适配不同屏幕时,只需要缩放整个容器而不影响物理计算精度。

EUI 布局方面,分数文本用 eui.Label 放在 uiLayer,发射按钮放在屏幕下半区。弹珠和挡板直接用 egret.Bitmap 或 egret.Shape,因为它们需要频繁更新坐标和角度,不走 EUI 的皮肤机制反而更省性能。源码里常见到混用两种方案,这没问题,但你要知道边界:EUI 组件不要放进 gameLayer 参与物理,否则皮肤刷新会干扰 body 同步。

2.3 物理参数选型:重力、碰撞体和反弹系数的推荐起点

物理系统初始化的参数,直接决定弹珠手感是“发飘”还是“发沉”。Egret 的 PhysicsSystem 里,重力用 Vec2 表示,单位是像素每二次方秒。弹珠台是竖直视角,重力应该指向屏幕下方,即 y 轴正方向。推荐起步值是 0, 980,这是接近 9.8 米每二次方秒换算到像素系的常用值;如果你觉得珠子下落偏慢,可以逐步加到 1200,不要一次性跳到 2000,否则挡板几乎来不及反应。

碰撞体的形状选择也是一个关键点。弹珠本身用圆形碰撞体(circle),半径比图片可视半径缩 2~4 像素——这个俗称“缩圈”的操作能大幅减少高速移动时的碰撞误判。挡板用矩形或胶囊形碰撞体,具体看挡板美术设计。一个常见误区是直接用图片宽高做碰撞体尺寸,边缘透明像素会带来大量无效碰撞,表现为弹珠还没碰到挡板视觉边缘就被弹开。

反弹系数(restitution)的取值区间建议:弹珠与普通障碍 0.6~0.8,与挡板 0.8~1.0,与底部墙壁 0.4~0.5。系数越高,反弹越接近弹性碰撞,但过高会让弹珠在窄通道里来回抖动,破坏节奏。摩擦力设为 0.2~0.4,挡板旋转时如果 fuctuacion 太低,弹珠会在挡板上打滑,手感像抹了油;太高则挡板“粘球”,判定分数时容易出问题。下面是一份可直接套用的初始化代码:

private initPhysics(): void { let physicsSystem = egret.PhysicsSystem.getInstance(); physicsSystem.initPhysics(egret.PhysicsSystem.PhysicsWorldType.BOX2D); // 重力向量 (0, 980),单位像素/秒^2,正 y 为屏幕下方 physicsSystem.gravity = new egret.Vec2(0, 980); // 物理调试开关:开发期置 true,能看到所有 body 轮廓 physicsSystem.debugDraw = true; }

注意 debugDraw 这个参数,发布前一定要关掉,否则手机上会多画一层碰撞体轮廓,既难看又增加绘制压力。initPhysics 只需要调用一次,之后的 body 创建、关节绑定、碰撞回调都通过 physicsSystem 实例操作。如果项目里还有其它场景,scene 切换时不要重复调用 initPhysics,否则物理世界会被重建,所有 body 引用全部失效,这类问题排查起来相当隐蔽。

3. 从“静止画面”到“能玩的弹珠台”:核心源码模块拆解

3.1 弹珠的生成与发射:从触控点到初速度的计算

弹珠在源码里一般被封装成一个 Ball 类,内部持有显示对象、body 引用和半径信息。生成时常见做法是预创建 5~10 个弹珠对象放进对象池,避免游戏过程中反复 new 和销毁——这是 H5 游戏性能优化的基本功,弹珠碰撞触发的 GC 停顿会直接体现在帧率曲线上。发射动作本身可以拆成两步:第一步把弹珠放到发射槽,第二步给它一个向上(屏幕上方)的初速度。

发射速度不是随便给的。你要考虑弹珠飞到顶部后还能不能落回挡板区域,如果初速度太小,弹珠在中途就开始下落,游戏会显得“没力气”;如果太大,弹珠直接飞出顶部边界,需要额外做边界约束。一个经过验证的参考范围是初速度 600~900 像素每秒,方向垂直向上。具体数值可以在源码里做成常量,方便调参。发射代码参考如下:

private shoot(ball: Ball): void { // 把弹珠放在发射槽:x 固定,y 在底部挡板下方 ball.body.position.x = this.launchSlotX; ball.body.position.y = this.launchSlotY; // 清除残余速度,避免上次碰撞的动量残留 ball.body.linearVelocity = egret.Vec2.ZERO; // 给一个向上的初速度:x 为 0,y 为负值(屏幕坐标系 y 向下) ball.body.linearVelocity = new egret.Vec2(0, -this.launchPower); }

这段代码里关键的一行是清除残余速度。很多源码坑就在于没做这一步:弹珠上一轮碰撞后速度可能还有残留,直接 teleport 回发射槽再发射,会出现弹珠“斜着飞出去”的诡异现象。linearVelocity 这个属性表示线速度,x 和 y 分量分离,方向正负对应屏幕坐标系——y 是负值才表示向上。launchPower 如果在调试时想即时调整,可以做成 egret.Tween 或直接监听触摸事件改数值,不必重新编译。

3.2 挡板控制的两种实现:触摸跟随与按键触发

挡板(flipper)是弹珠台里唯一由玩家主动控制的元素,它的交互手感直接决定游戏好不好玩。常见实现有两种方案:触摸跟随和按键触发。触摸跟班是挡板以底部为轴心旋转到手指所在角度,适合手机端触屏;按键触发是左右挡板分别映射到键盘按键,适合 PC 调试。源码里通常两种都做,用 egret.Capabilities 判断运行平台来决定启用哪套。

挡板旋转的核心是用关节(joint)限制旋转角度。Egret 物理系统支持 RevoluteJoint,你要设置 lowerAngle 和 upperAngle 两个参数,把挡板限制在 0 度到 60 度这样的小范围摆动拿。挡板控制实现参考如下:

private createFlipper(anchorX: number, anchorY: number, angle: number): egret.PhysicsBody { let flipper = this.createFlipperBody(anchorX, anchorY); // 旋转关节:锚点固定在底部 let joint = new egret.RevoluteJoint(flipper, this.wallBody, new egret.Vec2(anchorX, anchorY), new egret.Vec2(anchorX, anchorY)); joint.lowerAngle = 0; joint.upperAngle = angle * Math.PI / 180; joint.enableLimit = true; return flipper; }

enableLimit 设 true 时,挡板不会在物理推力下绕一整圈,碰撞时也更稳定。触控检测的注意点是:玩家手指按下的位置不一定在挡板上,你要在 touchBegin 里判断手指 x 坐标处于屏幕左半还是右半区,再决定驱动左挡板还是右挡板。对挡板施加力时用的是 setAngularVelocity 而不是直接改角度,这样物理引擎才能正确计算碰撞响应。

挡板触发频率在源码里值得注意:一次触摸事件连续触发多帧施力,会导致挡板反复抖动。常见解法是引入一个 cooldown 标志位,在关节角度回到初始位置前忽略新的触摸输入。这个标志位的阈值要留合理余量,否则快速连续点击挡板会完全失灵,玩家会以为按钮坏了。

3.3 碰撞监听与计分:BeginContact 的正确打开方式

计分系统是弹珠游戏里最直观的正向反馈,实现依赖物理引擎的碰撞回调。Egret 物理系统在发生碰撞时,会广播接触事件,你需要注册监听器,在回调里判断碰撞双方是谁和谁。注意:同一个碰撞对在一次接触中可能触发多次回调,如果不做去重,一次撞击可能加 3~5 次分。源码里常见的处理是在回调里检查接触体对(fixtureA/fixtureB)的组合是否已处理过。

计分实现参考:

private onContactBegin(contact: egret.PhysicsContact): void { let fixtureA = contact.getFixtureA(); let fixtureB = contact.getFixtureB(); let bodyA = fixtureA.getBody(); let bodyB = fixtureB.getBody(); // 判断碰撞双方是否为弹珠与得分区 if ((bodyA === this.ballBody && bodyB === this.scoreZoneBody) || (bodyA === this.scoreZoneBody && bodyB === this.ballBody)) { this.addScore(100); } }

addScore 里同时要播放音效和粒子特效。音效用 egret.Sound 加载,粒子用 egret.ParticleSystem,但注意不要让粒子系统直接挂在物理 body 上,否则粒子会跟着 body 一起位移,效果会穿帮。计分数值的产生可以放在服务端校验,也可以在客户端做临时显示,但如果要上排行榜,至少要在碰撞回调里把得分事件上报,不能只依赖客户端内存中的分数。

另一个常见坑是碰撞回调里不能直接销毁 body。Egret 物理系统的 contact 回调发生在物理步进过程中,此时修改 world 会导致内存崩溃。正确的做法是先打标记(flag),在下一帧的 enterFrame 里检查标记再移除 body。这部分逻辑在源码里表现为一个 pendingRemove 队列,如果你在拿到源码后发现没有这个队列,一定要补上,否则弹珠在碰撞瞬间消失大概率是团队删了回调代码。

3.4 动画与事件循环整合:让显示跟随物理而不是被物理牵着走

物理引擎会以固定频率推进世界,但显示层的渲染频率是跟随帧率走的。两者之间如果不同步,最直接的后果是弹珠在视觉上抖动或滞后于触控。标准解法是在 egret.Event.ENTER_FRAME 事件里,遍历所有参与物理的显示对象,把 body.position 赋给显示对象的 x/y,把 body.rotation 赋给显示对象的 rotation。同步代码参考:

private onEnterFrame(e: egret.Event): void { let bodies = this.physicsWorld.getBodyList(); for (let body of bodies) { let display = body.userData; if (display) { display.x = body.position.x; display.y = body.position.y; display.rotation = body.rotation * 180 / Math.PI; } } }

userData 是 Egret Physics 提供的一个附加属性,用来把 body 和显示对象关联起来。如果你拿到源码后发现作者用了别的方式关联(比如一个私有 Map),逻辑是一样的。注意 rotation 是从弧度换算到角度的,这个很容易漏掉,漏掉的话显示对象会以弧度值旋转,转几圈就整体歪了。

性能上,每一帧遍历所有 body 在弹珠台场景里完全可以接受——总物体数量一般不超过 30 个。但如果源码里还包含大量装饰用显示对象(比如背景星星),建议把它们放进另一个容器,不参与这个同步循环。另外,一旦游戏状态切到暂停(比如弹珠落底),要在 enterFrame 里跳过同步,并调用 physicsWorld.sleep() 让物理世界待机,否则弹珠会在暂停状态下继续微动,恢复时位置错乱。

4. 弹珠游戏源码避坑指南:5 个高频翻车点与排查路径

4.1 碰撞体“缩圈”没做好,弹珠还没碰到视觉边缘就被弹飞

现象:弹珠距离障碍物还有 2~3 个像素,就已经发生反弹。原因:碰撞体半径直接用了图片宽高的一半,透明像素参与了碰撞判定。解决:全局搜 radius 相关的初始化代码,把半径改为可视半径的 0.8~0.9 倍,并在物理调试模式下逐帧确认轮廓贴合。

4.2 重力方向反了,弹珠往屏幕上方飞

现象:发射后弹珠不是下落而是上升,或者障碍物“掉落”。原因:Egret 屏幕坐标系 y 轴向下,重力向量如果是 (0, -980) 就反了。解决:确认 gravity 设置代码里的 Vec2 第二个分量为正数。同时检查发射代码里的初速度方向,两者不要互相抵消。

4.3 计分重复触发,一次撞击加了多次分

现象:弹珠碰到一个得分区,分数连续跳多个 50/100。原因:contact 回调在物理步进里多次触发,代码没有做碰撞对去重。解决:在回调方法入口维护一个 Set,以 bodyA.bodyId + "_" + bodyB.bodyId 作为 key,处理过就跳过。碰撞结束后要清理这个 Set,否则同一次碰撞后续接触会被误屏蔽。

4.4 挡板反应迟钝,按下后有明显延迟

现象:手指点击挡板区域,挡板 0.1 秒后才开始转动。原因:事件监听挂在了具体挡板显示对象上,而挡板在小角度摆动,触点容易落在挡板外。解决:把触摸监听改到舞台(stage)级别,判断触点坐标落在左半区还是右半区,再驱动对应挡板。这是弹珠台源码里最常见的交互修正。

4.5 暂停和恢复时弹珠位置错乱

现象:切后台再回游戏,弹珠出现瞬移或卡进墙壁。原因:resume 时物理系统从休眠状态恢复,body 同步逻辑在暂停期间仍持续运行。 解决:在游戏的 pause 事件里调用 physicsWorld.sleep() 并跳过同步循环,resume 时先唤醒 world 再刷新 body 位置。不要只暂停显示层的 tween,物理世界的步进没有停。

5. 从“能玩”到“好玩”:弹珠台源码的三项进阶改造方案

弹珠游戏源码能让你快速起跑,但真正完成度高的产品,通常在源码基础上做了三件进阶事情。第一件:加入蓄力发射系统。点击发射区且按住不放时,弹珠会上下浮动,松开时按蓄力时间比例赋予初速度。这个效果通过 egret.Timer 或 tween 修改 launchPower 实现,代码量很小,对手感提升却很明显——玩家从“总是一样力度”进阶到“控制力度投放”,游戏的策略性立刻上线。

第二件:碰撞音效和命中停顿感。好的弹珠台游戏,每一次响亮碰撞都有短促的停顿感(hit-stop),它让碰撞的力度通过视觉节奏传达。实现方式是碰撞发生时暂停 physicsWorld 步进 50 毫秒再恢复。这个效果要控制频次,连续碰撞时只触发第一次,否则游戏全程卡顿。音效建议用多音轨预加载,一次碰撞事件触发两个短音频叠加,声音层次会更丰富。

第三件:多球模式。很多休闲玩家对弹珠台的期待是“球越多越爽”。在源码基础上增加第二、第三颗球的生成逻辑并不难——复用 Ball 对象池,发射时创建多个 body,但要检查这些球不会互相持续碰撞导致死循环。常见做法是多球之间禁用碰撞响应,用 contact 过滤实现,代码一行就能搞定。

对于要上线的项目,建议在进阶改造中加入物理参数动态配置面板:重力、反弹系数、挡板力度都做成可调变量,用玩家反馈数据反向调参。我个人的使用习惯是每次改完重力或反弹后,至少连续玩 20 局再换下一组参数,单局体验不足 3 分钟时看到的“手感问题”很可能是噪声。数值调优里最忌讳一边改参数一边下结论,弹珠台这种反馈快的游戏尤其如此,希望这个调试习惯能帮你少走几次弯路。本方案从碰撞体缩圈、重力方向到碰撞回调去重,把每一处你能踩的坑都提前标了出来;对拿到源码不知从哪下手的开发者来说,按文中顺序跑通后,再往里面加自己的关卡设计,成本比从零做要低得多,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 19:09:04

数据交付里最容易被跳过的一栏:元数据

同一份数据,为什么有人拿到就能用,有人还要再问一圈?这个问题在数据交接现场很常见。交付方把文件打包发出去,收数据的人打开一看,文件都在,可紧接着就冒出一连串疑问:这批数据是什么时候做的、…

作者头像 李华
网站建设 2026/10/15 19:06:59

研究问题与研究方法不匹配的学术写作困境分析

研究方法是回答研究问题的工具,方法选择必须服务于核心研究问题,但在论文写作实践中,研究问题与研究方法错配现象十分突出。部分学生盲目追逐热门研究方法,不考虑自身研究问题属性,质性研究问题强行套用量化模型&#…

作者头像 李华
网站建设 2026/10/15 19:06:16

Linux磁盘管理详解:从裸盘到挂载的完整链路与扩容实践

刚接手一台新服务器,插上数据盘准备做存储,结果发现事情远没有想象中简单——系统确实认出了这块盘,但应用怎么也写不进数据。折腾半天才发现,从一块裸盘到能被业务正常使用,中间隔着分区、格式化、挂载三道工序。这个…

作者头像 李华
网站建设 2026/10/15 19:05:55

C# USB摄像头稳定录制方案:AForge+FFmpeg工业级实践

简介:本资源是一份面向C#初学者与桌面应用开发者的USB摄像头调用实战项目,聚焦于Windows平台下视频录制与单帧图像保存的核心功能实现。资源提供DirectShow与Media Foundation双框架的代码实践路径,涵盖设备枚举、滤镜图构建、媒体流捕获、文…

作者头像 李华