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},
});
现在物理世界已就绪,接下来开始向其中添加内容。
创建一个平台
创建平台的步骤如下:
- 使用 world 对象在某位置创建刚体。
- 创建一个夹具(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-width 和 half-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
