简介:这份 Egret 弹珠游戏源码面向具备一定 TypeScript 基础、希望入门 HTML5 游戏开发的开发者,借助白鹭时代 Egret 引擎的跨平台渲染与资源管理能力,完整呈现一款弹珠小游戏的实现思路。压缩包共 110 个文件,约 1.06MB,以 28 个 js、26 个 ts 脚本为核心,配合 25 张 png 素材、11 个 map、10 个 json 配置、5 个 mp3 音效及 html 入口文件,覆盖逻辑、资源与配置各环节。源码围绕碰撞检测、物理模拟、Tween 动画、触摸交互、得分与关卡逻辑、音效管理等关键技术展开,public_playBall 目录中可看到主入口、弹珠实体、场景与音效等模块划分,便于理解游戏循环与实体行为组织方式。目前已有 1286 人学习,适合对照源码拆解弹珠运动与碰撞算法,快速积累 Egret 项目实战经验。
1. 从一份 egret 弹珠游戏源码说起:它到底能跑出什么效果
前阵子有个做小游戏外包的朋友接了个需求,客户要一款「弹珠台」玩法的休闲游戏,预算不高但要求两周出可玩版本。他翻了一圈开源仓库,最后锁定了一份 egret 弹珠游戏源码。我帮他一起拆了这套东西,发现它比想象中完整——不是那种只丢几个类文件的半成品,而是包含了弹珠发射、挡板碰撞、砖块消除、分数结算、关卡切换的完整闭环。如果你也在找一份能直接改、能上手的 egret 弹珠项目,这份源码值得花时间研究。
egret 这套引擎在国内 H5 小游戏圈子里沉淀了很多年,弹珠玩法又是物理碰撞的经典练手场景。这份源码解决的核心问题是:把「弹珠运动 + 碰撞检测 + 实时渲染」这条链路用 egret 的显示对象和帧循环串起来,让你不用从零搭物理框架。适合谁?一是想学 egret 实战但缺完整案例的前端,二是需要快速出弹珠类 Demo 接需求的外包,三是想把弹珠玩法移植到微信小游戏或抖音小游戏的开发者。下面我按「先看懂结构、再跑起来、最后改出自己东西」的顺序拆一遍。
2. 拆开源码看骨架:egret 弹珠的物理循环与显示层级
拿到一份游戏源码,最忌讳上来就改代码。我一般先花二十分钟把目录结构和核心类过一遍,搞清楚「谁在驱动谁」。egret 弹珠这类项目,骨架其实就三件事:一个主循环、一套碰撞判定、一层显示对象管理。把这三件事理清,后面改参数、换素材、加关卡都是顺水推舟。
2.1 主循环与帧驱动:egret 的 tick 是怎么转起来的
egret 引擎本身不强制你用某个游戏循环,但弹珠游戏必须有一个稳定的帧驱动来更新弹珠位置。这份源码里,主逻辑挂在egret.Event.ENTER_FRAME上,每帧做三件事:更新弹珠速度、检测碰撞、重绘显示。核心代码大致长这样:
// 主游戏类,继承 egret.DisplayObjectContainer class MarbleGame extends egret.DisplayObjectContainer { private ball: egret.Shape; // 弹珠显示对象 private vx: number = 4; // 水平速度,单位像素/帧 private vy: number = -6; // 垂直速度,负值向上 private gravity: number = 0.15; // 每帧重力增量 constructor() { super(); this.ball = new egret.Shape(); this.ball.graphics.beginFill(0xff0000); this.ball.graphics.drawCircle(0, 0, 8); this.ball.graphics.endFill(); this.addChild(this.ball); // 注册帧循环 this.addEventListener(egret.Event.ENTER_FRAME, this.onTick, this); } private onTick(): void { // 1. 重力加速 this.vy += this.gravity; // 2. 位置更新 this.ball.x += this.vx; this.ball.y += this.vy; // 3. 边界反弹(简化版) if (this.ball.x < 8 || this.ball.x > 480 - 8) { this.vx = -this.vx; } if (this.ball.y < 8) { this.vy = -this.vy; } // 4. 碰撞检测在下一节展开 this.checkCollision(); } }这段代码的逻辑说明:ENTER_FRAME是 egret 的帧事件,默认跟浏览器requestAnimationFrame对齐,大约 60 帧每秒。vx和vy是速度分量,gravity每帧叠加到vy上模拟重力。参数怎么调?gravity设 0.15 时弹珠下落偏「轻快」,想要更「沉」的手感可以加到 0.3;vx初始值决定发射角度,4 和 -6 的组合大约是 56 度仰角。注意:egret 的坐标系原点在左上角,y 轴向下为正,所以向上运动是负值,这个和数学坐标系相反,新手最容易在这里翻车。
2.2 碰撞检测:圆形弹珠和矩形挡板怎么判
弹珠游戏的核心手感来自碰撞判定。这份源码用的是「圆-矩形」检测,比像素级检测省性能,又比纯 AABB 精确。原理是:先找到矩形上离圆心最近的点,再算这个点到圆心的距离是否小于半径。代码实现:
// 圆与矩形碰撞检测 private checkCollision(): void { const ballR = 8; // 弹珠半径 // 遍历所有挡板/砖块 for (let brick of this.bricks) { // 找矩形上离圆心最近的点 let closestX = Math.max(brick.x, Math.min(this.ball.x, brick.x + brick.width)); let closestY = Math.max(brick.y, Math.min(this.ball.y, brick.y + brick.height)); // 算距离 let dx = this.ball.x - closestX; let dy = this.ball.y - closestY; let dist = Math.sqrt(dx * dx + dy * dy); if (dist < ballR) { // 碰撞响应:根据最近点方向反弹 if (Math.abs(dx) > Math.abs(dy)) { this.vx = -this.vx; // 水平反弹 } else { this.vy = -this.vy; // 垂直反弹 } // 把弹珠推回矩形外,防止卡住 this.ball.x += dx > 0 ? (ballR - dist) : -(ballR - dist); this.ball.y += dy > 0 ? (ballR - dist) : -(ballR - dist); brick.onHit(); // 触发砖块消除逻辑 } } }逻辑说明:closestX和closestY把圆心坐标「夹」到矩形范围内,得到矩形上离圆心最近的点。dx、dy是圆心到最近点的向量,dist是距离。当距离小于半径时判定碰撞。反弹方向用Math.abs(dx) > Math.abs(dy)判断——水平差大就水平反弹,垂直差大就垂直反弹,这是简化处理,真实物理应该按法线反射,但弹珠游戏里这种简化反而手感更「跟手」。
参数说明:ballR要和显示对象的drawCircle半径一致,否则会出现「看着没碰到却反弹」的玄学现象。brick.width和brick.height是砖块尺寸,如果砖块用了缩放,这里要乘上scaleX、scaleY,不然判定框和视觉框对不上。我一般会在调试阶段把碰撞框画出来,确认无误再隐藏。
2.3 显示层级与资源加载:谁盖在谁上面
egret 的显示列表是树形结构,后addChild的对象盖在先加的上面。弹珠游戏里层级顺序很关键:背景在最底、砖块居中、弹珠和挡板在上、UI 分数在最顶。这份源码用了一个layerMap来管理:
// 层级管理 private layerMap: { [key: string]: egret.DisplayObjectContainer } = {}; private initLayers(): void { const layers = ['bg', 'brick', 'ball', 'paddle', 'ui']; for (let name of layers) { let layer = new egret.DisplayObjectContainer(); this.layerMap[name] = layer; this.addChild(layer); // 按数组顺序添加,后面的盖前面 } } // 使用:把弹珠加到 ball 层 this.layerMap['ball'].addChild(this.ball);这样分层的好处是,改层级不用动业务代码,调layerMap的添加顺序就行。资源加载方面,源码用的是RES.getResAsync异步加载纹理,注意 egret 的RES模块需要在egret.runEgret之前配置好resource.json,否则会报「资源未找到」。常见做法是把图片、音效、配置表都挂在default.res.json里,用RES.loadConfig一次性加载。
3. 把源码跑起来:环境配置、编译与第一个可玩版本
看懂骨架之后,下一步是让它真正跑起来。egret 项目的环境配置有几个固定套路,但版本差异会带来不少坑。这一章我按「装工具 → 建项目 → 导源码 → 编译调试」的顺序走一遍,每一步都给出可抄的命令和配置。
3.1 环境准备:Egret Engine 与白鹭工具的版本选择
egret 的开发工具链主要是 Egret Engine(引擎运行时)和 Egret Wing(IDE,现在叫白鹭开发工具)。这份源码基于 egret 5.x 的 API 写的,如果你装的是 4.x 或更早版本,DisplayObjectContainer的部分方法会不兼容。我一般建议:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Egret Engine | 5.4.x | 源码 API 对齐,稳定 |
| Egret Wing | 4.x | 支持 5.x 项目结构 |
| TypeScript | 3.x | egret 5.x 内置,不用单独装 |
| Node.js | 14 LTS | 编译脚本依赖,别用太新的 |
安装步骤:先从 egret 官网下载 Egret Engine 安装包,装完后命令行执行egret能看到版本号就说明环境变量配好了。然后装 Egret Wing,用它打开项目目录。注意:不要用 VSCode 直接开 egret 项目,虽然能编辑代码,但编译和调试的配置容易乱,Wing 里一键编译更省事。
提示:如果你之前装过其他版本的 egret,先卸载干净再装,多个版本共存会导致
egret build找不到正确的引擎路径。
3.2 导入源码与目录结构对齐
拿到源码包后,先看根目录有没有egretProperties.json,这是 egret 项目的配置文件。标准结构大致是:
project/ ├── egretProperties.json # 引擎配置 ├── tsconfig.json # TS 编译配置 ├── src/ # 源码目录 │ ├── Main.ts # 入口 │ └── game/ # 游戏逻辑 ├── resource/ # 资源目录 │ ├── default.res.json # 资源清单 │ └── assets/ # 图片音效 └── bin-debug/ # 编译输出导入时最容易出问题的是egretProperties.json里的engineVersion字段。如果源码写的是5.4.0而你装的是5.2.0,编译会报版本不匹配。解决办法是打开这个文件,把engineVersion改成你本地实际装的版本号。另一个坑是resource/default.res.json里的路径,如果源码作者用的是绝对路径或者带盘符的路径,换机器后资源全加载不出来,要手动改成相对路径。
3.3 编译、调试与真机预览
环境对齐后,在项目根目录执行编译:
# 编译项目(debug 模式,带 sourcemap) egret build # 如果报错找不到 egret,用完整路径 # Windows: C:\Egret\EgretEngine\5.4.0\egret build # macOS: /Applications/EgretEngine/5.4.0/egret build # 启动本地服务器预览 egret runegret build会把src下的 TypeScript 编译成 JavaScript,输出到bin-debug。egret run会起一个本地 HTTP 服务,默认端口 3000,浏览器打开http://localhost:3000就能看到游戏。调试时按 F12 打开控制台,egret 的日志会打在 console 里。如果弹珠不动,先看 console 有没有报错,再看ENTER_FRAME有没有注册成功——常见原因是addEventListener写在了super()之前,导致监听器没挂上。
真机预览方面,egret 支持发布成 H5 后用手机浏览器打开。执行egret publish --runtime web会生成bin-release/web目录,把这个目录丢到任意静态服务器上就能访问。注意手机上的触摸事件和鼠标事件不一样,源码里如果只监听了egret.TouchEvent.TOUCH_MOVE,在 PC 上要用鼠标拖拽测试,得确认事件类型是否兼容。
4. 改出你自己的弹珠玩法:参数调优与功能扩展
能跑起来只是第一步,真正有价值的是把它改成你想要的样子。这一章讲三个最常改的方向:手感参数、关卡数据、以及加一个新道具。每个方向都给出具体改法和参数范围,你照着调就能出效果。
4.1 手感调优:速度、重力、反弹系数的黄金区间
弹珠游戏好不好玩,八成看手感。手感由三个参数决定:初速度、重力、反弹系数。这份源码里这三个值散落在不同文件,我一般会先把它们抽到一个GameConfig.ts里统一管理:
// GameConfig.ts 集中管理手感参数 export const GameConfig = { ballSpeed: 8, // 弹珠初速度,像素/帧 gravity: 0.15, // 重力加速度 bounceDamping: 0.98, // 反弹衰减,1 表示不衰减 paddleSpeed: 12, // 挡板移动速度 maxBallSpeed: 20, // 速度上限,防止穿透 };参数说明:ballSpeed在 6 到 12 之间比较合适,低于 6 弹珠太慢显得拖沓,高于 12 容易一帧穿过砖块导致「穿透」bug。gravity0.1 到 0.3 是常见区间,0.15 偏休闲,0.25 偏硬核。bounceDamping设 0.98 表示每次反弹损失 2% 速度,这样弹珠不会永远弹下去,最终会慢下来。maxBallSpeed是保险丝,防止速度累积到一帧位移超过砖块厚度。
调参的方法:改完值后重新egret build,在浏览器里试玩。我一般会开一个调试面板,实时显示当前速度和帧率,方便对比。如果发现弹珠偶尔「穿墙」,八成是速度太快加上帧率波动,把maxBallSpeed降到 15 以下通常能解决。
4.2 关卡数据外置:用 JSON 配置砖块布局
源码里砖块布局如果是硬编码在代码里的,改一关就要动代码,很不优雅。常见做法是把关卡数据抽成 JSON:
{ "level1": { "rows": 5, "cols": 8, "brickWidth": 50, "brickHeight": 20, "gap": 4, "layout": [ "11111111", "11111111", "11000011", "11111111", "11111111" ] } }layout里 1 表示有砖块,0 表示空位。加载时遍历这个二维数组,按brickWidth + gap算间距,生成砖块对象。这样加关卡只需要加一个 JSON 节点,不用碰逻辑代码。注意gap不要设太大,否则弹珠容易从缝隙里穿过去,视觉上也显得松散。我一般gap设 2 到 4 像素。
4.3 加一个「多球道具」:从碰撞回调到对象池
扩展玩法最直接的是加道具。以「多球」为例:弹珠碰到特定砖块后,分裂出两颗新弹珠。实现思路是维护一个弹珠数组,碰撞回调里判断砖块类型,是道具砖就克隆弹珠:
// 弹珠数组管理 private balls: Ball[] = []; private onBrickHit(brick: Brick): void { if (brick.type === 'multi') { // 克隆两颗,速度方向偏移 for (let i = 0; i < 2; i++) { let newBall = this.ballPool.get(); // 从对象池取 newBall.vx = this.ball.vx * (i === 0 ? 0.8 : 1.2); newBall.vy = this.ball.vy; this.balls.push(newBall); this.layerMap['ball'].addChild(newBall); } } brick.destroy(); }这里用了对象池ballPool,因为弹珠频繁创建销毁会触发 GC,导致帧率抖动。对象池的常见实现是维护一个空闲数组,get时取一个或新建,recycle时推回数组。注意多球之后碰撞检测要遍历所有弹珠,性能开销翻倍,弹珠数量建议控制在 5 颗以内。
5. 避坑与排查:egret 弹珠源码里最容易翻车的五个地方
这份源码我前后帮人调过三次,踩的坑高度集中。下面五条按「现象 → 原因 → 解决」写,你遇到问题时可以直接对号入座。
现象一:弹珠卡在砖块里抖动,出不来。原因是碰撞响应只反转了速度,没有把弹珠推出矩形。当弹珠速度慢、每帧位移小于重叠深度时,就会反复触发碰撞。解决:在反弹后加位置修正,把弹珠沿最近点方向推出ballR - dist的距离,代码在 2.2 节已经给出。
现象二:编译报「Cannot find module 'egret'」。原因是tsconfig.json里的types路径没指向本地引擎。解决:打开tsconfig.json,确认compilerOptions.types包含 egret 的.d.ts路径,通常是node_modules/egret/egret.d.ts或引擎安装目录下的types文件夹。
现象三:手机上触摸挡板没反应。原因是源码只监听了鼠标事件egret.MouseEvent,移动端不触发。解决:把事件类型换成egret.TouchEvent.TOUCH_MOVE,或者两个都监听。注意TOUCH_MOVE的坐标要用stageX、stageY,不要用localX,否则挡板位置会偏。
现象四:弹珠速度越来越快,最后穿墙。原因是反弹时没有做速度衰减,或者重力持续叠加没有上限。解决:加bounceDamping系数,每次反弹乘 0.98;同时给vy设一个上限,超过maxBallSpeed就截断。
现象五:发布到小游戏平台后白屏。原因是资源路径用了本地绝对路径,小游戏环境读不到。解决:检查default.res.json里所有url字段,改成相对路径,并且确认资源文件确实被打包进了发布目录。常见做法是用egret publish后检查bin-release里的资源是否完整。
6. 进阶技巧:用对象池和脏矩形把帧率稳住
源码跑通、玩法改完之后,如果要做成能上线的版本,性能是绕不过去的坎。egret 弹珠这类游戏,性能瓶颈通常不在逻辑计算,而在渲染和 GC。我分享两个实战中验证有效的技巧。
第一个是对象池。弹珠、砖块、粒子特效这些频繁创建销毁的对象,全部走对象池。实现上维护一个pool: any[],get时pop一个或新建,recycle时push回去并重置属性。注意回收时要removeChild并从显示列表移除,否则对象池里的对象还在渲染,白白消耗性能。我一般会在对象池里加一个active标记,get时置 true,recycle时置 false,帧循环里只更新 active 的对象。
第二个是脏矩形渲染。egret 默认每帧重绘整个舞台,但弹珠游戏里背景和静态砖块其实不用每帧重绘。可以在egretProperties.json里开启dirtyRect支持,然后对静态层调用cacheAsBitmap。这样引擎只重绘弹珠和挡板所在的区域,帧率能提升 30% 以上。注意cacheAsBitmap对频繁变化的显示对象不适用,砖块被消除后要手动refreshCache,否则会残留旧图像。
验证方法:在浏览器里按 F12 打开 Performance 面板,录制 10 秒游戏过程,看 FPS 曲线是否稳定在 55 以上。如果掉帧,先看 GC 频率,频繁 GC 说明对象创建太多,加对象池;再看渲染耗时,如果paint时间占比高,开脏矩形。我习惯在每次改完性能相关代码后,用同一段操作路径录三次,取平均帧率对比,避免单次波动误判。
从那以后我每次拿到一份游戏源码,都强制先跑一遍性能录制再动手改功能,不然改到一半发现帧率崩了,回头找原因更痛苦。希望这份拆解帮到你,少走点弯路。
本文还有配套的精品资源,点击获取