- 游戏开发
【免费下载链接】planck.js
2D JavaScript Physics Engine
planck.js 是一个以 TypeScript/JavaScript 实现的 2D 物理引擎(MIT 协议,源自 Box2D 的跨平台 HTML5 游戏开发移植)。本文以仓库根目录的 CHANGES.md 变更记录为主线,逐版还原 0.1 → 0.2 → 1.0.0 → 1.1.0 的演进脉络,并结合 src、vite.config.mts 等源码证据,解读每一次重构背后的设计动机与 API 迁移方式,帮助读者理解当前引擎的模块结构、事件系统与命名规范,并掌握从旧版 API 升级到新版的关键对照。
版本演进总览
当前仓库 package.json 版本为 1.5.0,而 CHANGES.md 记录了从早期移植到现代化重构的四个关键阶段:
| 版本 | 核心变化 | 性质 |
|---|---|---|
| 0.1 | 目录重排、类合并、标志位改布尔字段、事件系统引入 | 大范围架构重构 |
| 0.2 | TypeScript 定义加入、向量组合运算 API 重命名、关节构造器清理 | API 命名规范化 |
| 1.0.0 | 全量 TypeScript 迁移、性能优化 | 语言与运行时升级 |
| 1.1.0 | Vite 构建迁移 | 工程化升级 |
从变更记录看,planck.js 的演进方向非常清晰:先是内部架构去 Box2D 化(0.1),再是公开 API 规范化(0.2),随后是类型系统与构建工具链的现代化(1.0.0 / 1.1.0)。
0.1:架构重构——从"忠实移植"到"JavaScript 原生"
0.1 是信息量最大的一次版本,几乎重写了整个引擎的组织方式。以下按主题分组解读。
目录结构与文件组织
source code directory layout changed, classes moved around!
从当前仓库的目录布局可以反推这次调整的结果:
- 物理与碰撞基础类型集中在 src/common(
Vec2、Vec3、Mat22、Mat33、Rot、Transform、Sweep、Jacobian等); - 碰撞算法集中在 src/collision(
AABB、BroadPhase、DynamicTree、Distance、Manifold、Shape、TimeOfImpact),形状实现在 src/collision/shape(CircleShape、EdgeShape、PolygonShape、ChainShape、BoxShape及各Collide*配对碰撞函数); - 动力学与求解器集中在 src/dynamics(
Body、Fixture、Contact、Joint、World、Solver),关节实现位于 src/dynamics/joint。
b2Class renamed to Class
早期版本中所有类都继承自一个b2Class基类,0.1 起统一改名为Class。当前源码中已完全搜不到b2Class或GrowableStack的痕迹,说明这两项已彻底移除。
Dumping is removed, Drawing is moved to testbed
序列化/反序列化能力被拆分:场景"dump"功能被移除,绘图相关能力移入 testbed(即planck-with-testbed构建产物,见 vite.config.mts 的buildTestbed分支)。这也解释了package.json中./with-testbed导出子路径的存在——核心引擎与可视化测试台被明确解耦。
GrowableStack replaced with native array, Timer is changed
Box2D 风格的自增长栈数据结构被原生数组取代;计时器改为基于Date.now()的轻量实现,见 src/util/Timer.ts(now()直接返回Date.now(),diff(time)计算差值)。
数学与几何 API 拆分
Math is changed and split
Vec2.Min/Max renamed to Vec2.Upper/Lower and several other methods added
数学工具被拆分到多个文件(Vec2、Vec3、Mat22、Mat33、Rot、Transform、Sweep等,见 src/common),同时Vec2.Min/Max更名为Vec2.Upper/Lower,并补充了若干新方法。当前 Vec2.ts 中可以看到大量以matrix.*前缀出现的内部向量运算辅助,以及setVec2、add、setCombine、addCombine、subCombine、setMul、addMul、subMul等方法(见 Vec2.ts)。
TestOverlap moved from Collision to Distance
testOverlap从 Collision 模块移入 Distance 模块,当前位于 src/collision/Distance.ts:它通过设置input.useRadii = true调用Distance算法计算两形状最近距离,并以output.distance < 10.0 * EPSILON判定是否重叠。同时 Distance.ts 保留了 legacy 导出(Distance.testOverlap、Distance.Input、Distance.Output、Distance.Proxy、Distance.Cache),方便旧代码平滑迁移(Distance.ts)。
Distance method, DistanceInput/Output and SimplexCache merged into a stateful Distance class
原独立的Distance函数、DistanceInput、DistanceOutput、SimplexCache被合并进一个"有状态"的 Distance 模块。当前 Distance.ts 中DistanceInput、DistanceOutput、SimplexCache仍作为独立类存在(分别见 Distance.ts、Distance.ts、Distance.ts),但Distance本体变成了可复用的函数式接口,配合模块级复用对象(simplex、output、cache等)减少 GC 压力,内部统计stats.gjkCalls、stats.gjkIters也可用于基准测试(Distance.ts)。
Shape.computeDistanceProxy is added
形状基类Shape新增抽象方法computeDistanceProxy(src/collision/Shape.ts),各形状(CircleShape、EdgeShape、PolygonShape、ChainShape)分别实现,为 GJK 距离算法提供支持点查询代理。Distance内部通过shape.computeDistanceProxy(this, index)装载代理(Distance.ts)。
接触与碰撞架构合并
Contact and Collide classes merged, Contact registration/creation is changed
ContactSolver/VelocityConstraint/PositionConstraint merged with Contact
ContactManager/ContactListener merged into World (and Contact)
这三条变更合起来看:原先分散的碰撞、求解、管理职责被压缩。当前 src/dynamics/Contact.ts 一个文件内同时包含VelocityConstraintPoint、速度约束求解(initVelocityConstraint、solveVelocityConstraint)与位置约束求解(solvePositionConstraint、solvePositionConstraintTOI),即旧版ContactSolver/VelocityConstraint/PositionConstraint的逻辑已被合并进 Contact 类。而ContactManager/ContactListener的职责则并入 src/dynamics/World.ts 与 Contact。
Manifold files is added, WorldManifold.Initialize moved to Manifold.getWorldManifold
新增独立的 src/collision/Manifold.ts,原WorldManifold.Initialize变为Manifold.getWorldManifold。该方法接收世界变换与形状半径,将局部流形换算为世界坐标下的法线、接触点与分离距离(见 Manifold.ts),并处理e_circles、e_faceA、e_faceB三种流形类型。Manifold.ts 还静态导出clipSegmentToLine、ClipVertex、getPointStates、PointState(Manifold.ts)。
ContactFilter.ShouldCollide moved to Fixture?
碰撞过滤判定下沉到 Fixture 层,与 Fixture 的过滤数据(group、categoryBits、maskBits)耦合更紧密。碰撞配对阶段仍由 BroadPhase 的updatePairs驱动(src/collision/BroadPhase.ts),测试见 src/test/Collision.test.ts。
求解流程重组
Island/World.Solve/SolveTOI moved to new Solver
Position/Velocity correction objects assigned to owner bodies
孤岛(Island)管理与世界级求解(含 TOI 连续碰撞求解)被移入新的 src/dynamics/Solver.ts(其中定义TimeStep、ContactImpulse等求解输入输出类型),位置/速度矫正对象则归属到各自的 Body 上,消除了大量临时对象传递。
二进制标志位 → 布尔字段
binary flags changes to boolean fields (awakeFlag, autoSleepFlag, bulletFlag, fixedRotationFlag, activeFlag, enabledFlag, islandFlag, touchingFlag, filterFlag, bulletHitFlag, toiFlag, clearForces, newFixture, locked)
这是最影响内部实现的一处改动:原本用单个整数按位存储的十几个状态标志,全部改为独立的布尔字段。好处是代码可读性与调试友好度大幅提升,代价是需要逐一核对语义迁移。以 src/dynamics/Body.ts 为例,当前可见m_bulletFlag、m_autoSleepFlag、m_awakeFlag、m_activeFlag、m_fixedRotationFlag等字段,并通过isBullet/setBullet(Body.ts)、isSleepingAllowed/setSleepingAllowed(Body.ts)、isAwake/setAwake(Body.ts)、isActive/setActive(Body.ts)、isFixedRotation/setFixedRotation(Body.ts)成对暴露。
字符串类型系统
Body.Is[Type]/Set[Type] added
Shape, Body and Joint string types ('circle', 'dynamic', 'mouse-joint', etc.)
引擎从数值枚举走向字符串类型标识,这是一次面向开发者的重大 API 变更:
- 形状类型:
ShapeType = "circle" | "edge" | "polygon" | "chain"(src/collision/Shape.ts),getType()返回对应字符串; - 刚体类型:
BodyType = "static" | "kinematic" | "dynamic"(src/dynamics/Body.ts),并同时保留Body.STATIC/KINEMATIC/DYNAMIC静态常量与setStatic/setDynamic/setKinematic/setType/getType系列方法(Body.ts); - 关节类型:
DistanceJoint、FrictionJoint、GearJoint、MotorJoint、MouseJoint、PrismaticJoint、PulleyJoint、RevoluteJoint、RopeJoint、WeldJoint、WheelJoint各自定义static TYPE,如"distance-joint"(DistanceJoint.ts)、"mouse-joint"(MouseJoint.ts)、"wheel-joint"(WheelJoint.ts)等,Joint.getType()返回该字符串(Joint.ts)。
字符串类型的价值在于:序列化、调试输出、以及testbed的 UI 展示(关节样式匹配、形状渲染分支)都可以直接以可读字符串为键,无需维护枚举 ↔ 字符串的映射表。测试中可直接断言b1.getFixtureList().getType() === "circle"(src/test/Basic.test.ts)。
世界监听器 → 事件系统
World listeners changed to events: PostSolve, PreSolve, EndContact, BeginContact, SayGoodby to post-solve, pre-solve, end-contact, begin-contact, remove-body, remove-joint, remove-fixture
这是 0.1 中最具代表性的公开 API 变更:从"接口式监听器"转向"事件发布/订阅"。当前World以on(name, listener)/off(name, listener)/publish(name, ...)三件套实现事件机制(src/dynamics/World.ts),支持的事件包括:
| 事件名 | 回调参数 | 触发时机 |
|---|---|---|
begin-contact | (contact) | 两个 fixture 开始接触(World.ts) |
end-contact | (contact) | 两个 fixture 停止接触(World.ts) |
pre-solve | (contact, oldManifold) | 接触更新后、进入求解器前,可修改流形或禁用接触(World.ts) |
post-solve | (contact, impulse) | 求解完成后,用于读取冲量(World.ts) |
remove-body/remove-joint/remove-fixture | (body)/(joint)/(fixture) | 对应实体被显式或隐式移除时(World.ts) |
此外还扩展了add-body、add-joint、add-fixture三个新增事件(World.ts)。事件发布点在 Contact 与 World 内部:如beginContact/endContact/preSolve/postSolve均通过this.publish(...)触发(World.ts)。官方文档注明:这些回调中不能创建/销毁世界实体(World.ts),且同一时间步内同一接触可能收到多次回调。
回调类 → 函数
Callbacks classes changed to functions: DynamicTree.query DynamicTree.rayCast BroadPhase.updatePairs BroadPhase.rayCast BroadPhase.query World.queryAAB World.rayCast
Box2D 中以"回调对象 + 虚方法"实现查询的方式被替换为纯函数回调:
DynamicTree.query(aabb, callback)与DynamicTree.rayCast(input, callback)(src/collision/DynamicTree.ts);BroadPhase.query、BroadPhase.rayCast、BroadPhase.updatePairs(addPairCallback)(src/collision/BroadPhase.ts);World.queryAABB(aabb, callback)(src/dynamics/World.ts)与World.rayCast(point1, point2, callback)(World.ts),回调返回false可提前终止 AABB 查询,rayCast 回调返回数值可控制射线maxFraction的收缩。
0.2:API 命名规范化与 TypeScript 定义
向量组合运算重命名
wSet/wAdd/wSub(a, v, b, w) replaced with combine/setCombine/addCombine/subCombine(a, v, b, w)
wSet/wAdd/wSub(a, v) replaced with mul/setMul/addMul/subMul(a, v)
Vec2的加权赋值方法从带w前缀的模糊命名改为语义明确的组合运算命名。旧方法在 Vec2.ts 中仍保留但标记@deprecated,并内部委托给新方法:
wSet(a, v, b, w)→setCombine(a, v, b, w),语义为this = a*v + b*w;wSet(a, v)→setMul(a, v),语义为this = a*v(Vec2.ts);wAdd→addCombine/addMul,语义为this += a*v (+ b*w)(Vec2.ts);wSub→subCombine/subMul,语义为this -= a*v (+ b*w)(Vec2.ts)。
注意setCombine的注释特别说明this可能即是w(// this may be w),因此实现先计算局部变量再赋值,避免别名冲突——这是向量库常见的坑。此外Vec2.combine(a, v, b, w)静态方法也基于setCombine实现(Vec2.ts)。
关节构造器清理
Joints constructors cleanup
TypeScript definitions added
各关节构造器从"需要显式传入大量参数"改为统一的definition 对象 + 可选 bodyA/bodyB风格,例如 Joint.ts 支持两种构造形式:new Joint(def)或new Joint(def, bodyA, bodyB),其中bodyA可从def.bodyA解出。这一约定配合0.2引入的 TypeScript 定义文件,让关节创建更类型安全、更易读。
1.0.0:全量 TypeScript 迁移与性能优化
Typescript migration
Performance improvements
1.0.0 将引擎主体(含测试)从 JavaScript 全面迁移到 TypeScript。当前仓库结构可见完整证据:源码全部为.ts文件(src 下的*.ts)、测试使用 Vitest(*.test.ts位于 src/test与 src/serializer/test)、类型测试使用tsd(test-d/planck.test-d.ts)。
性能优化方面,从代码结构可以推断几个方向:
- 对象复用/池化:模块级复用
DistanceOutput、SimplexCache、Simplex等对象([Distance.ts](https://link.gitcode.com/i/397dcbd86cb532fc38a5bf147b3c63f4#L740)),Vec2、Mat22等临时对象通过内部matrix.*工具复用,src/util/Pool.ts 提供对象池; - 构建期常量折叠:
ASSERT、_ASSERT、CONSTRUCTOR_FACTORY在构建时通过 Vitedefine替换为false/true(vite.config.mts),运行时断言在生产构建中被移除; - 统计埋点:
stats.gjkCalls、stats.gjkIters等计数器(Distance.ts)服务于 benchmark 基准测试,npm run benchmark会结合 Git 提交信息对比性能(见 package.json)。
1.1.0:Vite 构建迁移
Vite build migration
构建工具链统一迁移到 Vite,由 vite.config.mts 完成以下工作:
- 双产物构建:核心引擎(
planck,入口 src/main.ts)与可视化测试台(planck-with-testbed,入口 testbed/main.ts)分别打包(vite.config.mts),同时输出 ES(.mjs)与 UMD(.js)两种格式(vite.config.mts); - 类型声明打包:通过
vite-plugin-dts-bundle-generator生成planck.d.ts(vite.config.mts),供package.json的exports字段分发(package.json); - License 注入:
rollup-plugin-license在每个构建产物头部注入 MIT License 横幅(vite.config.mts); - 压缩产物:
npm run build额外用 terser 生成planck.min.js与planck-with-testbed.min.js(package.json),分别提供给 jsDelivr/unpkg CDN 使用(package.json)。
从 1.1.0 起,npm run dev/npm run testbed启动 Vite 开发服务器并加载 testbed 交互式测试台,npm run test运行 Vitest 测试,npm run docs:dev基于 Vitepress 预览 docs 文档站(package.json)。
从 0.x 迁移到当前版本的快速对照
综合 0.1 与 0.2 的破坏性变更,整理出面向升级者的核心对照表:
| 旧写法(0.x) | 新写法(当前) | 依据 |
|---|---|---|
Vec2.wSet(a, v, b, w) | vec.setCombine(a, v, b, w) | Vec2.ts |
Vec2.wAdd(a, v) | vec.addMul(a, v) | Vec2.ts |
Vec2.wSub(a, v, b, w) | vec.subCombine(a, v, b, w) | Vec2.ts |
Vec2.Min/Max | Vec2.Upper/Lower | CHANGES.md 0.1 条目 |
| 数值类型枚举 | 字符串类型:"circle"、"dynamic"、"mouse-joint"等 | Shape.ts、Body.ts、MouseJoint.ts |
WorldManifold.Initialize(...) | manifold.getWorldManifold(wm, xfA, radiusA, xfB, radiusB) | Manifold.ts |
| 二进制标志位 | 布尔字段 +isXxx/setXxx | Body.ts |
监听器接口(BeginContact、SayGoodbye等) | 事件:world.on("begin-contact", ...)、world.on("remove-body", ...) | World.ts |
| 回调对象 | 函数回调:world.queryAABB(aabb, cb)、world.rayCast(p1, p2, cb) | World.ts、World.ts |
| 关节多参数构造 | 定义对象构造:new RevoluteJoint({...}, bodyA, bodyB) | Joint.ts |
延伸阅读
- CHANGELOG.md:更细粒度的逐版本变更说明;
- docs/core-concepts.md 与 docs/world/simulation.md:理解 World 事件与求解流程的使用语境;
- src/dynamics/World.ts 与 src/common/Vec2.ts:事件系统与向量 API 的第一手实现;
- example 目录下的 60+ 个示例(如
Breakable.ts、Pinball.ts、RayCast.ts):新 API 的实际调用范式; - vite.config.mts 与 package.json:构建与发布产物全貌。
- 游戏开发
【免费下载链接】planck.js
2D JavaScript Physics Engine
相关推荐
Qlib 版本演进全解析:从 0.1.0 到 0.8.0 的架构变迁与技术脉络
Qlib 版本演进全解析:从 0.1.0 到 0.8.0 的架构变迁与技术脉络 Qlib 是一款面向 AI 量化研究的开源平台,其核心代码自 0.1.0 起步,
金融科技人工智能机器学习数据分析强化学习VCMI 引擎版本演进全解析:从 1.8.0 开发版到 0.x 时代的完整技术变更脉络
VCMI 引擎版本演进全解析:从 1.8.0 开发版到 0.x 时代的完整技术变更脉络 本文以仓库根目录的 ChangeLog.md https://link.
游戏开发flow-for-vscode 版本演进全景解析:从 LSP 迁移、安全加固到 TypeScript 重写的完整技术脉络
flow for vscode 版本演进全景解析:从 LSP 迁移、安全加固到 TypeScript 重写的完整技术脉络 Flow 是 JavaScript 的
开发工具静态分析代码质量
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考