资讯动态

Forge2D 实战指南:在 Flame/Flutter 项目中使用 Box2D v3 物理引擎

发布时间:2026/9/15 22:56:19 来源:尧图企业网站定制
Forge2D 实战指南在 Flame/Flutter 项目中使用 Box2D v3 物理引擎【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flameForge2D 是本仓库flame 引擎生态中负责物理模拟的核心库——它不是纯 Dart 移植而是 Box2D v3 的 Dart 绑定在移动端与桌面端以原生代码运行、在 Web 端以 WebAssembly 运行且不依赖 Flame 也能在任意 Dart 项目中使用。读完本文你将掌握 Forge2D 的初始化流程、世界/刚体/形状/关节的完整 API 用法、事件轮询与射线/AABB 查询方式以及 0.14 到 0.15 重写版的迁移要点并了解如何通过 flame_forge2d 桥接包将其接入 Flame 游戏。Forge2D 是什么Forge2D 为 Box2D 接入——桥接包将本文描述的这些概念封装成 Flame 组件BodyComponent、Forge2DWorld等。如果你是从 forge2d 0.14 升级请同时参考官方 迁移指南本文最后一节也会系统梳理重写带来的 API 变更。快速开始创建你的第一个物理世界在pubspec.yaml中添加依赖并创建世界import package:forge2d/forge2d.dart; Futurevoid main() async { // 创建任何世界之前必须完成初始化详见下文初始化一节。 await initializeForge2D(); final world World(gravity: Vector2(0, -10)); final ground world.createBody(BodyDef(position: Vector2(0, -1))); ground.createShape(Polygon.box(50, 1)); final ball world.createBody( BodyDef(type: BodyType.dynamic, position: Vector2(0, 10)), ); ball.createShape( Circle(radius: 0.5), ShapeDef(material: SurfaceMaterial(restitution: 0.8)), ); for (var i 0; i 120; i) { world.step(1 / 60); print(ball.position.y); } world.destroy(); }这段代码做了四件事await initializeForge2D()——初始化引擎Web 端加载 WASM 模块World(gravity: ...)——创建物理世界gravity接收一个Vector2world.createBody(BodyDef(...))——创建刚体静态地面用默认的BodyType.static球体显式指定BodyType.dynamicbody.createShape(geometry, shapeDef)——给刚体挂形状地面是一个 50×1 的Polygon.box球是一个半径 0.5 的Circle并为其指定带 0.8 弹性系数的SurfaceMaterial。之后按固定时间步1/60秒循环调用world.step推进模拟。值得注意的是Forge2D 独立使用时世界遵循 Box2D 的 y 轴向上y-up约定所以向下的重力是负 y 值(0, -10)而flame_forge2d桥接包会自动翻转使其匹配 Flame 的 y 轴向下坐标系。初始化initializeForge2D()的正确用法await initializeForge2D()必须在第一个World创建之前完成原生平台立即返回Web 平台需要获取并实例化 Box2D WebAssembly 模块在初始化完成前创建世界会抛出StateError。由于同一份代码要运行在所有平台上请在启动时始终 await 一次await initializeForge2D();Web 模块的查找顺序为Dart Web 工具链提供的包资源路径 → Flutter Web 自动打包的资源 → 页面旁边的box2d.wasm。常见场景无需任何额外配置如果你把模块托管在别处可以通过initializeForge2D(wasmUri: ...)指定地址。从源码看Forge2DGame已在onLoad中代为 await见 forge2d_game.dartoverride Futurevoid onLoad() async { await initializeForge2D(lengthUnitsPerMeter: lengthUnitsPerMeter); await super.onLoad(); }因此基于Forge2DGame的游戏无需手动调用——除非你在游戏之外创建World。同时要注意重写onLoad的子类必须在创建任何刚体前await super.onLoad()否则在 Web 端会抛异常。初始化还接受一个可选参数lengthUnitsPerMeter桥接包中通过Forge2DGame构造器传入它告诉引擎你的世界以多少长度单位等于 1 米用于按比例平移 Box2D 以绝对长度表达的各种容差。句柄模型显式销毁World、Body、Shape、Chain以及各种关节都是廉价的、类值value-like的句柄它们只是原生引擎中对象的 id。因此销毁是显式的用完后对句柄调用destroy()world.destroy()释放整个模拟当句柄可能已经指向被销毁的对象时先用isValid检查。从 forge2d_world.dart 可以看到桥接包中物理世界是懒创建的首次访问physicsWorld时才创建因为 Web 上初始化是异步的同时世界组件不会自动销毁物理世界Box2D 允许同时存在的世界数量有限因此彻底不用时应手动调用physicsWorld.destroy()。形状系统ShapeGeometry ShapeDef刚体上承载的是形状。形状由不可变的ShapeGeometryCircle、Capsule、Segment、Polygon四选一加上可选的ShapeDef创建ShapeDef持有density密度碰撞过滤器category / mask 过滤事件标志如enableContactEvents、enableSensorEventsSurfaceMaterial——表面材质包含friction摩擦、restitution弹性等。静态关卡几何的线段链用body.createChain(ChainDef(points: ...))创建。Capsule是 0.15 新增的几何类型0.14 没有对应物它由两个圆心center1、center2和radius描述。常用形状构造速查0.14 写法0.15 写法CircleShape()..radius rCircle(radius: r, center: c)EdgeShape()..set(a, b)Segment(point1: a, point2: b)PolygonShape()..set(vertices)Polygon(vertices)PolygonShape()..setAsBoxXY(w, h)Polygon.box(w, h)ChainShape()..createChain(points)body.createChain(ChainDef(points: points))ChainShape()..createLoop(points)body.createChain(ChainDef(points: points, isLoop: true))链Chain的约束至少需要 4 个点链是单面的——实体面在绕行方向的右侧因此闭合环应逆时针绕行开放地面链应从右到左列点。开放链的首尾点是用于平滑碰撞的幽灵锚点ghost anchors不参与可碰撞线段所以一个 4 点开放链只产生 1 条线段链的线段可通过chain.segments读取。形状的几何体可以读回用于渲染或检查shape.geometry返回封闭的ShapeGeometry类型配合 Dart 的模式匹配可以完整解构switch (shape.geometry) { case Circle(:final center, :final radius): case Capsule(:final center1, :final center2, :final radius): case Segment(:final point1, :final point2): case Polygon(:final points, :final radius): }这正是 body_component.dart 中renderShape的用法——BodyComponent对每种几何类型分别调用renderCircle、renderCapsule、renderSegment、renderPolygon绘制调试轮廓。注意默认摩擦值变了0.14 的FixtureDef默认摩擦为 0而 0.15 的SurfaceMaterial默认摩擦为 0.6。Box2D 合成接触摩擦的方式是sqrt(frictionA * frictionB)所以过去依赖旧默认值的一对接触原本是无摩擦的现在不再如此。凡是你依赖旧默认行为的地方请显式传SurfaceMaterial(friction: 0)。事件与查询轮询式事件接触、传感器和刚体移动事件在每次step之后从世界中轮询world.contactEvents——begin、end、hit三个列表。begin 事件携带接触法线normal和接触点points取代了旧的Manifoldhit 事件携带point、normal和approachSpeed接近速度用于衡量撞击强度world.sensorEvents——传感器重叠的begin和end每条事件各带一个sensor和一个visitor形状world.bodyMoveEvents——报告本步移动过的刚体。事件只会为主动通过ShapeDef事件标志选择加入opt-in的形状生成例如enableContactEvents、enableSensorEvents、enableHitEvents、enablePreSolveEvents。另外end事件可能引用到期间已被销毁的形状使用前必须检查Shape.isValid。接触监听器示例0.14 → 0.15// 0.14ContactListener 回调 class MyListener extends ContactListener { override void beginContact(Contact contact) { ... } } world.setContactListener(MyListener()); // 0.15轮询事件 body.createShape(Circle(radius: 1), ShapeDef(enableContactEvents: true)); world.step(1 / 60); for (final event in world.contactEvents.begin) { // event.shapeA, event.shapeB, event.normal, event.points }旧的preSolve变为世界级回调world.preSolveCallback返回是否应在该步求解该接触需要形状开启ShapeDef.enablePreSolveEvents。旧的Contact类整体移除contact.isTouching()不再需要begin 事件本身就意味着开始接触contact.getWorldManifold(...)由 begin 事件的normal与points取代。查询直接返回结果回调类查询改为返回结果的方法。注意射线现在用起点 平移向量表达而不是两个点// 0.14 world.raycast(MyCallback(), start, end); // 0.15 final hit world.castRayClosest(start, end - start); // 最近命中可能为 null final allHits world.castRayAll(start, end - start); // 全部命中由近到远 world.castRay(start, end - start, (hit) 1); // 回调式由返回值控制每个RayHit携带shape、point、normal、fraction。castRay的回调通过返回值控制后续行为-1忽略该命中0停止命中点的fraction将射线裁剪到该命中点1继续不裁剪见 forge2d_world.dart 的桥接封装。其他查询world.queryAABB(callback, aabb)→world.overlapAabb(aabb)返回重叠的形状列表AABB 类由AABB改名为Aabb爆炸效果world.explode(ExplosionDef(...))world.clearForces()已删除——Box2D v3 中力按步应用。关节八种内置类型Forge2D 支持八种关节distance距离、filter过滤、motor电机、mouse鼠标、prismatic棱柱、revolute旋转/铰链、weld焊接、wheel车轮。齿轮、滑轮、绳索、摩擦和恒容关节在 Box2D v3 中不存在因此被移除FilterJoint仅禁用两刚体间的碰撞和WheelJoint是新增的。创建方式统一把对应JointDef传给世界上的类型化方法例如world.createRevoluteJoint(RevoluteJointDef(bodyA: ..., bodyB: ...))移除则调用joint.destroy()。每个关节的详细参数与示例见 flame_forge2d 关节文档这里择要说明DistanceJoint约束两刚体上两点保持固定距离可当作无质量刚性杆enableSpring: true后由hertz越高越硬和dampingRatio0 无阻尼1 临界阻尼构成弹簧/阻尼器还可用enableLimit/minLength/maxLength限制距离范围、用enableMotor驱动距离。警告不要使用零或过短的长度。FilterJoint不施加任何约束只关闭两刚体间的碰撞。MotorJoint控制两刚体间相对运动通过linearOffset、angularOffset指定目标位置/旋转偏移用maxForce、maxTorque限制驱动力correctionFactor[0,1]调节响应速度——过高会过冲振荡过低响应迟钝。MouseJoint把刚体上的点拉向光标位置无旋转限制maxForce通常取体重的倍数target在拖拽过程中通过targetsetter 更新。文档特别警告它不适合追求精确、即时响应的玩法这类需求建议改用 kinematic 刚体。PrismaticJoint沿 bodyA 上固定的轴做单自由度平移需要localAxisA定义运动轴线支持enableLimit/lowerTranslation/upperTranslation限位与enableMotor/motorSpeed/maxMotorForce电机运行期用joint.translation、joint.speed读取状态。RevoluteJoint强制两刚体共享铰链点单自由度相对旋转支持enableLimit/lowerAngle/upperAngle角度限位弧度与电机运行期用joint.angle读取角度。WeldJoint锁定两刚体的全部相对运动可加弹簧参数linearHertz、angularHertz等变软。若要做可断裂物体建议用单刚体多形状而非依赖 WeldJoint 的柔性。WheelJoint两个自由度——沿 bodyA 中弹簧轴平移 bodyB 旋转专为车辆悬挂设计localAxisA定义悬挂轴。关节 API 变化要点0.14 → 0.15由world.createJoint(joint)world.destroyJoint(joint)改为类型化创建方法 joint.destroy()定义上的initialize(...)辅助方法被移除锚点改为局部坐标可用body.localPoint(worldAnchor)计算final joint world.createRevoluteJoint( RevoluteJointDef( bodyA: bodyA, bodyB: bodyB, localAnchorA: bodyA.localPoint(anchor), localAnchorB: bodyB.localPoint(anchor), ), ); joint.destroy();弹簧参数改名为hertz原frequencyHz且弹簧一般需显式enableSpring开启访问器从getX()/setX()方法改为 getter/setter如joint.motorSpeed 2、joint.angle限位 setter 改为命名参数joint.setLimits(lower: 0, upper: pi)世界坐标锚点joint.anchorA/joint.anchorB被移除需要时用joint.bodyA.worldPoint(joint.localAnchorA)自行计算例如渲染关节时。平台支持与构建要求Forge2D 支持 Android、iOS、macOS、Windows、Linux 和 Web原生平台捆绑的 Box2D 源码由 Dart build hooks 编译因此需要 C 工具链——iOS/macOS 需要 Xcode、Android 需要 NDK、Windows 需要 Visual Studio Build Tools、Linux 需要 clang 或 gccWeb使用捆绑的 Box2D WebAssembly 构建由 Dart Web 工具链自动提供服务并自动打包进 Flutter Web 构建产物。此外0.15 起Dart SDK 版本下限为 3.12对应 Flutter 3.44。在 Flame 中使用flame_forge2d 桥接包虽然 Forge2D 可独立使用但在 Flame 游戏中应使用 flame_forge2d 桥接包它把上述概念封装为 Flame 组件。Forge2DGame 与单位换算Forge2DGame是专门的FlameGame子类内置带Forge2DViewfinder的CameraComponent。物理世界以米为单位视口用metersToPixels默认 100决定一米渲染多少像素。世界布局建议保持在 Box2D 调优的范围内运动刚体约 0.110 米1 米为最佳。通过构造器super(metersToPixels: yourScale)或运行期game.metersToPixels yourScale调整缩放视口的zoom叠加在metersToPixels之上默认 1用于正常的镜头缩放除渲染外一切都以米为单位刚体位置、camera.viewfinder.position、camera.visibleWorldRect以及事件报告的局部坐标。Forge2DGame的构造器还接收gravity默认Vector2(0, 10)y 向下与 Flame 坐标系一致和可选的lengthUnitsPerMeter。从 forge2d_world.dart 可见Forge2DWorld的默认重力是(0, 10.0)与独立 Forge2D 的 y-up 相反如果传入WorldDef其默认重力是 Box2D 的 y-up(0, -10)必须同时显式设置重力gravity参数优先于definition?.gravity。单位与缩放为什么缩小世界会出问题Box2D 的部分容差是绝对长度而非形状尺寸的比例。在世界以远小于 1 米的尺度布局时这些容差不再可忽略反而主导了模拟容差默认值在只有 1 米宽的世界里的影响Tolerances.speculativeDistance0.02 m接触在世界的 2% 范围内被报告WorldDef.restitutionThreshold1 m/s永远不会有反弹WorldDef.hitEventThreshold1 m/s永远不产生 hit 事件BodyDef.sleepThreshold0.05 m/s刚体还在移动时就睡着了WorldDef.maxContactPushSpeed3 m/s重叠刚体被猛烈推开Tolerances.aabbMargin0.05 m宽相界远大于形状本身其中第一条最常被当作 bug 报告Box2D 会对正在接近但尚未接触的形状预生成接触点这正是阻止快速刚体穿透、消除碰撞抖动的原因也因此beginContact会在仍有最多speculativeDistance可见间隙时就触发——小于该容差的刚体将与其邻居永久接触。BodyComponent在调试模式下检测到这种过小的运动刚体时会打印一次警告实现见 body_component.dart。正确做法是把世界放大而不是缩小。放大长度时务必同比例放大重力否则一切看起来都像在糖浆里慢动作长度与重力同乘一个因子S后模拟的时间节律完全不变物理量缩放因子长度、位置、半径、速度、重力、加速度S密度、摩擦、弹性、阻尼、角速度1不变质量S²力、线性冲量S³扭矩、转动惯量、角冲量S⁴时间1不变例原来 1 米高、0.02 米球、重力 9.81 的世界放大为 10 米高、0.2 米球、重力 98.1行为完全相同但稳稳落在 Box2D 调优范围内再把metersToPixels除以同一因子保持屏幕尺寸不变。若布局实在无法改动可通过Forge2DGame构造器传入lengthUnitsPerMeter例如人物身高 0.04 个单位、把它当作 1 米的人时传 0.04让上表中的所有绝对容差随之平移class MyGame extends Forge2DGame { MyGame() : super(lengthUnitsPerMeter: 0.04); }注意这是 Box2D 内部的进程级设置一旦物理世界存在就不能更改只能通过构造器传入多个同时运行的游戏必须对此达成一致请求不同值会抛出StateError而不是静默破坏模拟。Forge2DWorldForge2DWorld是所有BodyComponent生存的世界Forge2DGame默认持有名为world的实例。它同时支持BodyComponent与普通 Flame 组件混排物理刚体由 Forge2D 引擎更新其余组件按FlameGame常规方式更新。切换世界game.world Forge2DWorld()复用世界并保留物理状态设game.world.destroyBodiesOnRemove false默认为true移除时销毁所有刚体底层物理世界通过world.physicsWorld暴露用于访问桥接包未包装的 API如创建关节、轮询原始事件流每次update执行physicsWorld.step(dt, subStepCount: subStepCount)并分发接触/传感器事件forge2d_world.dartsubStepCount默认为 4替代了旧版独立的速度/位置迭代次数Forge2DWorld还提供了castRayClosest、castRay、castRayAll、overlapAabb、preSolveCallback、customFilterCallback的面向 Flame 的封装。BodyComponent 与接触回调BodyComponent是物理刚体的 Flame 组件包装创建方式有三种重写createBody()通过构造器传BodyDef可选加ListShapeSpecShapeSpec把ShapeGeometry与可选ShapeDef配对赋值this.bodyDef与this.shapeSpecs。final ball BodyComponent( bodyDef: BodyDef(type: BodyType.dynamic), shapeSpecs: [ ShapeSpec( Circle(radius: 0.5), ShapeDef(material: SurfaceMaterial(restitution: 0.8)), ), ], );要点renderBody默认为true否则刚体不可见需要自绘时置为false可在BodyComponent上添加子组件如动画但不要把刚体作为其他组件的子组件——Forge2D 没有嵌套刚体概念应加到顶层物理世界world.add(Weapon())而非add(Weapon())快速子弹请设isBullet true以避免隧穿。接触回调通过ContactCallbacksmixin 使用beginContact(Object other, Contact contact)、endContact(...)。其前提是刚体或接触形状的userData被设置为ContactCallbacks实例且相关形状开启了ShapeDef.enableContactEvents传感器还需enableSensorEvents。默认的createBody()在userData是ContactCallbacks时会自动开启这些标志见 body_component.dart但重写createBody()时必须自行设置userData默认null为 null 时接触事件被忽略。从 0.14 迁移API 全景对照0.15 是一次彻底重写从纯 Dart 移植的 Box2D 2.x 变成 Box2D v3 的绑定整个公开 API 都变了。除上文已述内容外还有以下关键变更World 与 Body 的命名与语义变化world.stepDt(dt)→world.step(dt, subStepCount: 4)不再有world.bodies请自行追踪创建的刚体或用world.bodyMoveEvents旋转用Rot一对余弦/正弦表示BodyDef(angle: a)→BodyDef(rotation: Rot.fromAngle(a))body.setTransform(position, rotation)接收Rotbody.angle仍然存在刚体改名worldCenter→worldCenterOfMassgetLocalCenter()→localCenterOfMasssetAwake(value)→isAwake valuegetInertia()→rotationalInertiabodyType→typeresetMassData()→applyMassFromShapes()setMassData(data)→massData dataworldVector(v)→rotation.rotate(v)localVector(v)→rotation.inverseRotate(v)BodyDef改名allowSleep→enableSleepbullet→isBulletactive→isEnabled。每刚体重力gravityScale从Vector2变为double且是世界重力的乘数——零重力世界里它无效。旧 Dart 移植特有的gravityOverride没有对应物要给刚体自定义重力向量设gravityScale: 0或保持世界重力为零然后在每个 update 中自行施力例如在BodyComponent中override void update(double dt) { super.update(dt); body.applyForce(customGravity * body.mass); }力在每个 step 后会被清除所以必须每次更新都施加而不是只施一次若希望静止刚体保持睡眠可传wake: false——这正是常规重力的行为。userData 与粒子系统userData存储在 Dart 侧在世界中维护而非原生指针其所有者句柄被销毁时清除LiquidFun 粒子系统不属于 Box2D v3已被移除。如果游戏依赖它请停留在 forge2d 0.14。使用 Flame 桥接包的用户还应一并阅读 flame_forge2d 迁移指南它覆盖了BodyComponent、Forge2DWorld与接触回调在桥接层的变化。小结Forge2D 把 Box2D v3 以 Dart 绑定的形式带给了 Flutter/Dart 生态独立使用需要await initializeForge2D()、显式句柄销毁与 y-up 坐标约定在 Flame 中则通过Forge2DGame/Forge2DWorld/BodyComponent桥接包获得组件化的体验并注意用米布局、用metersToPixels决定屏幕大小这一核心原则。事件从监听器改为轮询、查询直接返回结果、关节与刚体 API 全面重命名构成了 0.15 重写的主要迁移成本。上述所有行为均可在本仓库源码中验证forge2d_world.dart、forge2d_game.dart、body_component.dart 与 forge2d_viewfinder.dart配套的关节用法与示例可查阅 flame_forge2d 文档 及其 关节说明。【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价