☰
Plinko 编程挑战 Part 2:用 Matter.js 为物理模拟添加边界与底部分格篮筐
2026/10/13 1:51:25 网站建设 项目流程
  • 教程
  • 文档
  • 示例工程

【免费下载链接】website-archive

Archive of the Coding Train website (first version)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

本文基于 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.jsParticle:动态圆刚体,含离屏判定
plinko.jsPlinko:静态图钉刚体
boundary.jsBoundary:静态矩形墙体刚体
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角色
ParticleBodies.circle(x, y, r)restitution: 0.5, friction: 0, density: 1particle从顶部下落、可被模拟推动的动态粒子
PlinkoBodies.circle(x, y, r)restitution: 1, friction: 0, isStatic: trueplinko静止图钉,粒子在其上弹跳
BoundaryBodies.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--; // 回退下标,避免跳过元素 } }

这里有两个要点:

  1. World.remove必不可少:仅从particles数组中splice只会丢失 JS 引用,刚体仍留在 Matter.js 世界中继续参与碰撞与求解,会造成“幽灵刚体”和内存增长;必须显式调用World.remove(world, body)将其从世界中摘除;
  2. 正向遍历 +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)

项目地址:https://gitcode.com/gh_mirrors/we/website-archive
点击查看免费下载

相关推荐

上一篇:解决Refined-GitHub插件中隐藏非活跃部署功能失效的完整指南
下一篇:ZyPlayer:你的私人影视管家,一个应用搞定全网视频资源

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询