Hello World

原文地址:https://piqnt.com/planck.js/docs/hello-world#creating-a-world

本节将通过一个简单示例,逐步介绍如何创建物理世界、一个平台和一个小箱子。

创建世界

每个 Planck.js 程序都从创建 World 对象开始。World 是物理系统的核心,负责管理对象、物理交互并运行模拟。

创建世界只需实例化 World 类,可以选择是否设置重力加速度:

let world = new World({
    gravity: {x: 0, y: -10},
});

现在物理世界已就绪,接下来开始向其中添加内容。

创建一个平台

创建平台的步骤如下:

  1. 使用 world 对象在某位置创建刚体。
  2. 创建一个夹具(fixture),通过它在刚体上附加一个形状。

第一步:通过向 world 对象传入刚体属性来创建一个平台对象。通过刚体属性可指定平台的类型和初始位置:

let platform = world.createBody({
    type: "static",
    position: {x: 0, y: -10},
    angle: Math.PI * 0.1
});

刚体默认为“静态(static)”类型。静态刚体不会与其他静态刚体碰撞,且不可移动。

第二步:创建夹具 Fixture,在刚体上附加一个形状 Shape

platform.createFixture({
    shape: new Edge({x: -50, y: 0}, {x: +50, y: 0}),
});

形状仅具有几何属性(如顶点或半径),不包含物理属性。夹具用于将形状附加到刚体,并为刚体添加物理属性(如密度、摩擦系数等)。一个刚体可以拥有任意数量的形状。

形状的几何坐标是相对于刚体本地坐标系的。夹具本身没有位置和角度。因此当刚体移动时,其上的所有夹具/形状会随之移动。不支持在刚体上单独移动或修改形状。

Planck.js 是刚体引擎,其许多设计基于刚体模型假设。会变形的物体并非刚体,违反此原则将导致诸多问题。因此请勿移动或修改附加到刚体上的形状。

每个夹具——包括静态夹具都必须具有一个父刚体,但你可以将所有静态夹具附加到同一个静态刚体上。

静态刚体的质量默认为零,因此无需指定密度。后续我们将学习如何使用夹具属性自定义物理行为。

创建动态箱子

创建动态箱子的步骤与平台类似。主要区别在于,除了尺寸不同外,动态刚体需要指定质量属性。

首先使用 createBody 创建刚体。由于刚体默认为静态类型,构建时需要显式设置刚体的类型 type 为动态:

let body = world.createBody({
    type: "dynamic",
    position: {x: 0, y: 4}
});

注意:若希望刚体能响应力的作用而运动,必须将其类型 type 设置为动态 dynamic

接着通过夹具定义创建并附加一个箱形形状:

body.createFixture({
    shape: new Box(1.0, 1.0),
    density: 1.0,
    friction: 0.3,
});

注意这里将密度设为 1.0(默认密度为 0)。设置夹具的密度会自动更新刚体的质量。同时将形状的摩擦系数设为 0.3。

重要提示:动态刚体应至少包含一个密度非零的夹具,否则会出现异常行为。

您可以在一个刚体上添加任意数量的夹具,每个夹具都会贡献总质量。

箱形尺寸是以 half-widthhalf-height(类似圆形的半径)。因此本例中地面箱子的宽度(x 轴)和高度(y 轴)均为 2 个单位。

单位系统

Planck.js 默认调校为使用米、千克、秒单位制。因此您可以将尺寸单位视为米。当物体尺寸接近典型现实物体大小时,Planck.js 通常能发挥最佳效果。例如,一个桶的高度约 1 米。由于浮点运算的限制,使用 Planck.js 模拟冰川或尘埃颗粒的运动并不合适。如果您使用其他单位(如像素),可以修改 Settings.lengthUnitsPerMeter 的值。例如,若采用像素单位且桶高为 80 像素,可将 lengthUnitsPerMeter 设置为 80。

演示

原文并没有演示,以下代码是自己补充的:

                    <script>
     /* =========================
     基本参数
     ========================= */
     const pl = planck;
     const canvas = document.getElementById('c');
     const ctx = canvas.getContext('2d');

     const SCALE = 30;      // 1 m = 30 px
     const TIME_STEP = 1 / 60;

     /* =========================
     世界
     ========================= */
     const world = new pl.World({
        gravity: pl.Vec2(0, 0)
     });

     /* =========================
     边界
     ========================= */
     const wall = world.createBody();
     wall.createFixture(pl.Edge(pl.Vec2(-12, -9), pl.Vec2(12, -9)));
     wall.createFixture(pl.Edge(pl.Vec2(-12, 9), pl.Vec2(12, 9)));
     wall.createFixture(pl.Edge(pl.Vec2(-12, -9), pl.Vec2(-12, 9)));
     wall.createFixture(pl.Edge(pl.Vec2(12, -9), pl.Vec2(12, 9)));

     /* =========================
     小球(同密度)
     ========================= */
     const density = 1.0;

     // 球 1:1 kg
     const b1 = world.createDynamicBody(pl.Vec2(-6, 0));
     b1.createFixture(pl.Circle(0.5), {
         density,
         restitution: 1.0
     });
     b1.setLinearVelocity(pl.Vec2(6, 0));

     // 球 2:3 kg → 半径 √3 倍
     const r2 = Math.sqrt(3) * 0.5;
     const b2 = world.createDynamicBody(pl.Vec2(0, 0));
     b2.createFixture(pl.Circle(r2), {
         density,
         restitution: 1.0
     });

     // 球 3:1 kg
     const b3 = world.createDynamicBody(pl.Vec2(6, 0));
     b3.createFixture(pl.Circle(0.5), {
         density,
         restitution: 1.0
     });

     /* =========================
     坐标变换
     ========================= */
     function toCanvas(v) {
         return {
             x: canvas.width / 2 + v.x * SCALE,
             y: canvas.height / 2 - v.y * SCALE
         };
     }

     /* =========================
     调试渲染
     ========================= */
     function drawWorld() {
         ctx.clearRect(0, 0, canvas.width, canvas.height);

         for (let body = world.getBodyList(); body; body = body.getNext()) {
             for (let f = body.getFixtureList(); f; f = f.getNext()) {
                 const shape = f.getShape();

                 ctx.strokeStyle = '#0f0';
                 ctx.lineWidth = 1;

                 if (shape.getType() === 'circle') {
                     const p = toCanvas(body.getWorldPoint(shape.m_p));
                     const r = shape.m_radius * SCALE;

                     ctx.beginPath();
                     ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
                     ctx.stroke();

                     // 朝向线
                     const a = body.getAngle();
                     ctx.beginPath();
                     ctx.moveTo(p.x, p.y);
                     ctx.lineTo(
                     p.x + Math.cos(a) * r,
                     p.y - Math.sin(a) * r
                     );
                     ctx.stroke();
                 }

                 if (shape.getType() === 'edge') {
                     const v1 = toCanvas(body.getWorldPoint(shape.m_vertex1));
                     const v2 = toCanvas(body.getWorldPoint(shape.m_vertex2));
                     ctx.beginPath();
                     ctx.moveTo(v1.x, v1.y);
                     ctx.lineTo(v2.x, v2.y);
                     ctx.stroke();
                 }
             }
         }
     }

     /* =========================
     MouseJoint 拖拽
     ========================= */
     let mouseJoint = null;
     const mouse = pl.Vec2();

     canvas.addEventListener('mousedown', e => {
         const rect = canvas.getBoundingClientRect();
         mouse.x = (e.clientX - rect.left - canvas.width / 2) / SCALE;
         mouse.y = -(e.clientY - rect.top - canvas.height / 2) / SCALE;

         world.queryAABB(pl.AABB(mouse, mouse), f => {
             const body = f.getBody();
             if (body.isDynamic()) {
                 mouseJoint = world.createJoint(pl.MouseJoint({
                 maxForce: 1000 * body.getMass()
                 }, world.createBody(), body, mouse));
                 return false;
             }
                return true;
         });
     });

     canvas.addEventListener('mousemove', e => {
         if (!mouseJoint) return;
         const rect = canvas.getBoundingClientRect();
         mouse.x = (e.clientX - rect.left - canvas.width / 2) / SCALE;
         mouse.y = -(e.clientY - rect.top - canvas.height / 2) / SCALE;
         mouseJoint.setTarget(mouse);
     });

     window.addEventListener('mouseup', () => {
         if (mouseJoint) {
             world.destroyJoint(mouseJoint);
             mouseJoint = null;
         }
     });

     /* =========================
     控制
     ========================= */
     let paused = false;

     document.getElementById('play').onclick = () => {
         paused = !paused;
         document.getElementById('play').textContent =
         paused ? '▶ 播放' : '⏸ 暂停';
     };

     document.getElementById('step').onclick = () => {
         if (paused) {
             world.step(TIME_STEP);
             drawWorld();
        }
     };

     /* =========================
     主循环
     ========================= */
     function loop() {
         if (!paused) {
            world.step(TIME_STEP);
         }
         drawWorld();
         requestAnimationFrame(loop);
     }
     loop();
 </script>
    

由于 planck.js 自带的渲染器 Testbed 文档不全,好像只能绘制到全屏,所以让 AI 实现了类似的绘制功能,在画面添加了 3 个质量分别为 1 kg、3 kg、1 kg 的弹性小球,模拟了完全弹性碰撞的物理情境,你也可以使用鼠标拖动小球并投掷。


发布时间:2025/12/14 上午10:58:13  阅读次数:473

2006 - 2026,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号