- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
本文基于 Coding Train 网站的 Plinko 编程挑战第 62.2 部分(CC_062_plinko示例)展开,讲解在已有粒子下落模拟的基础上如何完成三件事:为画布添加边界墙、删除飞出屏幕的刚体(body)、在底部构造粒子落位的篮筐分格。读完本文,你可以理解 Matter.js 中静态墙体与动态刚体的构建方式、物理世界的刚体生命周期管理(World.add/World.remove),以及碰撞事件的 label 匹配机制,并能直接打开仓库中的示例代码在本地运行。
1. Part 2 的任务定位
原始文档 062.2-plinko.md 对该部分的核心描述是:为草图添加边界(boundaries)、删除屏幕外的刚体(delete offscreen bodies),并在底部加入供粒子落位的篮筐(buckets)。它是四部分系列中的第二部分:
- Part 1(062.1-plinko.md):用 Matter.js 物理引擎 + p5.js 绘制,建立 Plinko 物理模拟的基础;
- Part 2(本文主题):边界、离屏刚体清理、底部篮筐;
- Part 3(062.3-plinko.md):加入随机性,试验图钉尺寸、颜色与物理参数;
- Part 4(062.4-plinko.md):加入音效,粒子每次撞到图钉时播放铃声。
对应源码位于 CodingChallenges/CC_062_plinko/P5/ 目录:
| 文件 | 职责 |
|---|---|
| index.html | 页面入口,按顺序加载 p5.js、p5.sound、Matter.js 与四个脚本 |
| sketch.js | 主逻辑:setup/draw、碰撞事件、生成与清理粒子 |
| particle.js | Particle:动态圆刚体,含离屏判定 |
| plinko.js | Plinko:静态图钉刚体 |
| boundary.js | Boundary:静态矩形墙体刚体 |
| ding.mp3 | 铃声素材(preload中加载,当前源码中播放被注释) |
2. 依赖与运行方式
index.html 通过 CDN 加载三个库,再依次引入四个本地脚本:
<script src="https://cdn.jsdelivr.net/npm/p5@1.4.1/lib/p5.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/p5@1.4.1/lib/addons/p5.sound.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.12.0/matter.min.js"></script> <script src="sketch.js"></script> <script src="particle.js"></script> <script src="plinko.js"></script> <script src="boundary.js"></script>版本前提:p5.js 1.4.1(含 p5.sound 插件)与 Matter.js 0.12.0。本地运行时,在CodingChallenges/CC_062_plinko/P5/目录下启动一个静态 HTTP 服务(如python -m http.server)后在浏览器打开index.html即可;由于 sketch.js 的preload()中使用了loadSound('ding.mp3'),走file://协议加载音频容易失败,因此推荐用 HTTP 方式访问。
从源码结构看,这些文件没有使用 ES module,而是通过全局作用域协作:sketch.js顶层声明的全局变量world、particles、plinkos、bounds,以及 Matter.js 解构出的Engine、World、Events、Bodies,会被后续脚本中的构造函数直接引用(例如 boundary.js 中的World.add(world, this.body))。因此index.html中的引入顺序只需保证 p5 与 matter 先于四个脚本加载即可。
3. 三种刚体:构建参数与职责
本示例中的物理世界由三种刚体构成,各自通过不同的Bodies选项区分行为:
| 类 | 形状 | Matter 选项 | label | 角色 |
|---|---|---|---|---|
Particle | Bodies.circle(x, y, r) | restitution: 0.5, friction: 0, density: 1 | particle | 从顶部下落、可被模拟推动的动态粒子 |
Plinko | Bodies.circle(x, y, r) | restitution: 1, friction: 0, isStatic: true | plinko | 静止图钉,粒子在其上弹跳 |
Boundary | Bodies.rectangle(x, y, w, h) | isStatic: true | 未设置 | 静态墙体:地面与分格隔板 |
关键源码:
// particle.js(节选) function Particle(x, y, r) { this.hue = random(360); var options = { restitution: 0.5, // 恢复系数:决定碰撞后的反弹强度 friction: 0, // 无摩擦 density: 1 // 默认密度 }; x += random(-1, 1); // 初始 x 加 ±1px 抖动,避免多个粒子完全重叠 this.body = Bodies.circle(x, y, r, options); this.body.label = 'particle'; this.r = r; World.add(world, this.body); // 加入物理世界 }// plinko.js(节选) function Plinko(x, y, r) { var options = { restitution: 1, // 完全弹性反弹,让粒子几乎不损失能量地弹开 friction: 0, isStatic: true // 静态体:不参与动力学,位置永不被模拟改变 }; this.body = Bodies.circle(x, y, r, options); this.body.label = 'plinko'; this.r = r; World.add(world, this.body); }几个值得注意的参数选择:
- 粒子
restitution: 0.5而图钉restitution: 1:Matter.js 中一对碰撞体的最终弹性取两者组合的结果,图钉设为 1 让粒子撞钉时反弹更“脆”,更接近 Plinko 棋盘上木钉的物理手感; isStatic: true:静态体不参与位置/速度求解,因此图钉和墙体数量多(本例 120 个图钉 + 14 面墙)也不会带来等价的求解开销;x += random(-1, 1):粒子每 20 帧在同一个固定坐标 (300, 0) 生成(见下文),若不做抖动,相邻粒子将沿完全相同的路径下落,视觉上“排队”。这一行是后续 Part 3 系统性地加入随机性的起点。
4. 边界与底部篮筐的几何构造
这是 Part 2 文档所述“add boundaries”和“add buckets”的直接实现,全部位于 sketch.js 的setup()中。
4.1 地面
var b = new Boundary(width / 2, height + 50, width, 100); bounds.push(b);地面是一块width × 100的矩形,中心位于height + 50(画布 700px 高的情况下即 y = 750),因此其上表面恰好落在画布底边 y = 700,向下延伸到画布外。粒子落在地面后即停驻在篮筐底部。
4.2 分格隔板(构成篮筐)
for (var i = 0; i < cols + 2; i++) { var x = i * spacing; var h = 100; var w = 10; var y = height - h / 2; var b = new Boundary(x, y, w, h); bounds.push(b); }其中cols = 11、spacing = width / cols = 600 / 11 ≈ 54.5(sketch.js 第 21-22 行)。循环共创建13 面竖直隔板(i从 0 到 12),宽度 10px、高度 100px,中心 y =height - 50,即占据画布最底部 100px(y 从 600 到 700)。按spacing等距排布后:
- 第一面隔板位于 x = 0(画布左边缘),第 12 面恰好位于 x = 600(画布右边缘),最后一面 x ≈ 654.5 落在画布之外;
- 相邻隔板之间形成一个宽约 44.5px(54.5 − 10)的格口,正好让直径 20px 的粒子落位并堆叠;
- 画布可见区域内共 11 个格口,即文档所说的 “buckets”——粒子穿过图钉阵后,会分别落入底部这些分格中。
隔板与地面在 y = 700 处恰好相接,形成封闭的落位槽。
4.3 图钉阵与篮筐的配合
图钉阵列(Part 1 建立的布局)参数为rows = 10、每行cols + 1 = 12个图钉,偶数行整体右移spacing / 2形成交错排布(sketch.js 第 52-63 行)。钉阵最底一行位于 y ≈ 545,而篮筐区从 y = 600 开始,两者之间留有约 55px 的“直落段”,粒子在此段内基本不再发生碰撞,落格分布主要由最后一排钉附近的反弹决定。
4.4 墙体的绘制
Boundary 的 show 方法 用白色填充和描边、rectMode(CENTER)按刚体中心绘制矩形,使渲染几何与碰撞几何严格一致——这也是 Matter.js 示例中推荐的做法:始终以this.body.position为渲染基准,而非自己另存一份坐标。
5. 刚体生命周期:周期性生成与离屏删除
Part 2 文档的第二项任务 “delete offscreen bodies” 由三处代码协作完成。
5.1 生成
function newParticle() { var p = new Particle(300, 0, 10); particles.push(p); } function draw() { background(0, 0, 0); if (frameCount % 20 == 0) { newParticle(); // 每 20 帧生成一个粒子 } Engine.update(engine, 1000 / 30); // 固定时间步长 ... }Engine.update(engine, 1000 / 30)以约 33.3ms 的固定时间步长推进物理世界,使模拟速度与渲染帧率解耦;粒子则以固定频率(每 20 帧)从画布顶部中心生成,形成持续不断的粒子流。
5.2 离屏判定
Particle.isOffScreen 定义了“离开世界”的条件:
Particle.prototype.isOffScreen = function() { var x = this.body.position.x; var y = this.body.position.y; return x < -50 || x > width + 50 || y > height; };横向各留 50px 余量(大于粒子半径 10,允许粒子略微越界而不误删),纵向则是一旦y > height(低于画布底边)即判定离场。正常情况下粒子会停驻在篮筐内,不会触发y > height;该分支主要覆盖从最外侧格口翻出、越过画布边缘等异常路径。
5.3 从世界中移除
draw 循环中的清理逻辑:
for (var i = 0; i < particles.length; i++) { particles[i].show(); if (particles[i].isOffScreen()) { World.remove(world, particles[i].body); // 从物理世界移除,停止求解 particles.splice(i, 1); // 从 JS 数组移除 i--; // 回退下标,避免跳过元素 } }这里有两个要点:
World.remove必不可少:仅从particles数组中splice只会丢失 JS 引用,刚体仍留在 Matter.js 世界中继续参与碰撞与求解,会造成“幽灵刚体”和内存增长;必须显式调用World.remove(world, body)将其从世界中摘除;- 正向遍历 +
i--:splice(i, 1)会使后续元素前移,若不将下标回退一格,被删元素的“后继”将跳过本帧的show()与离屏检查。
由于粒子流是持续的(每 20 帧新增一个),若无此清理机制,世界中的刚体数量将随运行时间无界增长,物理求解开销也随之线性上升。离屏删除是这类“无限生成型”模拟的必备生命周期管理手段。
6. 碰撞事件与 label 匹配机制
sketch.js 第 35-49 行 在setup()中注册了全局碰撞监听:
function collision(event) { var pairs = event.pairs; for (var i = 0; i < pairs.length; i++) { var labelA = pairs[i].bodyA.label; var labelB = pairs[i].bodyB.label; if (labelA == 'particle' && labelB == 'plinko') { //ding.play(); } if (labelA == 'plinko' && labelB == 'particle') { //ding.play(); } } } Events.on(engine, 'collisionStart', collision);Matter.js 的collisionStart事件回调会携带event.pairs,每对碰撞体以bodyA/bodyB给出,但先后顺序不定,所以代码对两种排列都做了判断。这正是第 3 节中为Particle与Plinko设置label的用途:label 是 Matter.js 官方推荐的碰撞对象区分手段(配合collisionFilter可实现更细粒度的过滤)。
当前源码中ding.play()处于注释状态,但preload()已经通过ding = loadSound('ding.mp3')完成了音频加载(注意此处未加var,ding成为全局变量)。这与文档脉络一致:音效属于 Part 4 的内容,而事件骨架与资源加载在当前源码中已就位。
7. 从源码结构看系列演进与可动手的扩展点
对照各部分文档的描述,可以推断当前 CC_062_plinko/P5 中的源码是四部分系列的累积结果:图钉阵与粒子流(Part 1)、Boundary类与离屏清理(Part 2)、粒子初始 x 的random(-1, 1)抖动(Part 3“加入随机性”的最简形式)、以及已加载但暂未启用的铃声(Part 4)。文档中world.gravity.y = 2一行被注释掉(sketch.js 第 33 行),说明重力保持 Matter.js 默认值。
在本文档所述范围内,示例提供了几个直接的参数化扩展入口,全部可在源码中找到对应位置:
- 修改
cols/rows(sketch.js 第 21-22 行):改变钉阵规模与篮筐格数,注意spacing = width / cols会连带改变隔板位置; - 调整粒子与图钉的
restitution/friction:改变弹跳节奏与滞留时间; - 取消 碰撞回调 中
ding.play()的注释:在用户触发浏览器声音策略后即可获得撞钉音效; - 调整 isOffScreen 的 50px 阈值 或生成周期
frameCount % 20:控制清理灵敏度与粒子流密度。
8. 小结
Part 2 在 Part 1 的粒子模拟之上完成了三件相互独立又彼此配套的事:用Boundary静态矩形搭建地面与 13 面分格隔板,形成 11 个可见篮筐格口;用frameCount % 20维持稳定粒子流、以固定时间步长Engine.update(engine, 1000 / 30)驱动模拟;通过isOffScreen判定加World.remove加数组splice的三段式清理,保证刚体集合不会无限膨胀。配合基于label的collisionStart监听,这套代码构成了一个可长期运行、资源可控的 Plinko 物理场景,也为此后的随机性调参与音效增强(Part 3、Part 4)预留了干净的接入点。完整示例代码见 CodingChallenges/CC_062_plinko/P5/sketch.js、particle.js、plinko.js、boundary.js 与 index.html。
- 教程
- 文档
- 示例工程
【免费下载链接】website-archive
Archive of the Coding Train website (first version)
相关推荐
Coding Train 编码挑战 62.3:Matter.js Plinko 模拟的粒子随机化与物理调参实战
Coding Train 编码挑战 62.3:Matter.js Plinko 模拟的粒子随机化与物理调参实战 本篇技术指南围绕 Coding Train 网站
教程文档示例工程用 Matter.js 与 p5.js 打造 Plinko 弹珠机模拟:Coding Challenge 62.1 源码实战解析
用 Matter.js 与 p5.js 打造 Plinko 弹珠机模拟:Coding Challenge 62.1 源码实战解析 本篇技术指南以 Coding
教程文档示例工程Konado物理模拟:为对话场景添加真实感
Konado物理模拟:为对话场景添加真实感 引言:从静态对话到动态沉浸 在传统视觉小说中,对话场景往往是静态的——角色立绘固定位置,背景静止不变,整个画面缺乏生
游戏开发开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考