尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Forge2D 0.14 到 0.15 迁移完全指南:从 Dart 移植版到 Box2D v3 原生绑定(Flame 项目)

Forge2D 0.14 到 0.15 迁移完全指南:从 Dart 移植版到 Box2D v3 原生绑定(Flame 项目) Forge2D 0.14 到 0.15 迁移完全指南从 Dart 移植版到 Box2D v3 原生绑定Flame 项目【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame导读Forge2D 0.15 是一次从底层推倒重来的大版本升级它不再是 Box2D 2.x 的纯 Dart 移植而是改为 Box2D v3 的 Dart 绑定在移动端与桌面端以原生代码运行、在 Web 端以 WebAssembly 运行因此整个公开 API 全部改变。本文以 Flame 仓库中的 Forge2D 迁移指南 为核心骨架结合packages/flame_forge2d的源码实现系统梳理 Fixture→Shape、接触监听→轮询事件、查询回调→返回值、关节创建方式、World/Body 属性重命名等全部破坏性变更并给出可直接复制的迁移前后对照代码。阅读本文后你将掌握从 forge2d 0.14 / flame_forge2d 0.19 平滑升级到 forge2d 0.15 / flame_forge2d 0.20 的完整步骤以及踩坑点默认摩擦系数变化、单向链条、世界尺度问题的规避方法。若你通过 Flame 使用 Forge2D请同时阅读 flame_forge2d 迁移指南它专门覆盖BodyComponent、Forge2DWorld与接触回调在桥接层上的变化本文与它互为补充。注意粒子系统LiquidFun不属于 Box2D v3已在 0.15 中被移除。如果你的游戏依赖粒子物理请继续停留在 forge2d 0.14 / flame_forge2d 0.19。初始化是强制要求先initializeForge2D()再创建 World0.14 中创建World无需任何前置调用0.15 中必须在第一个World创建之前完成await initializeForge2D()await initializeForge2D(); final world World(gravity: Vector2(0, -10));在原生平台上该方法立即返回在 Web 上它负责加载并实例化 Box2D 的 WebAssembly 模块未完成初始化就创建 world 会抛出StateError。因此无论目标平台是什么都应在启动阶段统一await一次。从源码看Forge2DGame已经替你处理了这一步forge2d_game.dart 的onLoad先执行await initializeForge2D(lengthUnitsPerMeter: lengthUnitsPerMeter)再await super.onLoad()而Forge2DWorld的物理世界是懒创建的forge2d_world.dart 在首次使用时才new forge2d.World(...)正是为了让初始化异步完成。所以基于Forge2DGame的游戏无需手动调用但如果你自行创建Forge2DWorld或裸的World包括测试代码必须先await initializeForge2D()否则 Web 端会抛异常。另一个易踩的坑Forge2DGame子类若重写onLoad必须在创建任何 body 之前await super.onLoad()否则 Web 端直接崩溃。平台要求Dart SDK 下限3.12对应 Flutter 3.44。可在 flame_forge2d/pubspec.yaml 中确认sdk: 3.12.0 4.0.0、flutter: 3.44.0且依赖forge2d: ^0.15.1。原生平台随包分发的 Box2D 源码通过 Dart build hooks 编译因此需要 C 工具链——iOS/macOS 用 XcodeAndroid 用 NDKWindows 用 Visual Studio Build ToolsLinux 用 clang 或 gcc。Web 平台随包分发的 WebAssembly 模块在常见托管场景下会被自动找到Dart Web 工具链的服务路径、Flutter Web 自动打包的 asset、页面旁的box2d.wasm除await initializeForge2D()外无需额外配置若模块托管在别处可用initializeForge2D(wasmUri: ...)指定。Fixture 消失Body 直接承载 Shape0.15 中Body不再持有Fixture而是持有Shape。Shape由不可变的ShapeGeometry与可选的ShapeDef创建摩擦与弹性恢复系数restitution移入ShapeDef.material即SurfaceMaterial// 迁移前 final shape CircleShape()..radius 5; body.createFixture(FixtureDef(shape, restitution: 0.8, friction: 0.4, density: 2)); // 迁移后 body.createShape( Circle(radius: 5), ShapeDef( material: SurfaceMaterial(restitution: 0.8, friction: 0.4), density: 2, ), );警告默认摩擦系数变了FixtureDef的默认摩擦系数是0无摩擦SurfaceMaterial的默认摩擦系数是0.6。Box2D 按sqrt(frictionA * frictionB)混合接触双方的摩擦系数所以原来依赖旧默认值一侧为 0而表现为无摩擦的接触对现在不再无摩擦。凡是你依赖旧默认行为的地方请显式传入SurfaceMaterial(friction: 0)。flame_forge2d 的迁移文档migration.md也专门用警告强调了这一点。属性与读取方式body.fixtures→body.shapesfixture.testPoint→shape.testPoint参数仍为世界坐标渲染或检查形状时用shape.geometry读回几何数据返回 sealed 类型ShapeGeometryswitch (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): }在桥接层BodyComponent的渲染钩子同样从“Fixture 时代”改为“Shape 时代”renderFixture(Canvas, Fixture)→renderShape(Canvas, Shape)renderEdge→renderSegment并新增renderCapsulerenderChain被移除链条段统一走renderSegment渲染详见 flame_forge2d 迁移指南。形状构造对照表迁移前迁移后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))Capsule是新增形状0.14 没有对应物。链条的规则变化最隐蔽链条现在至少需要 4 个点并且是单向的——实体面在绕行方向的右侧。因此环路按逆时针绕行开放地面链条从右向左列出。对于开放链条首尾两个点是用于平滑碰撞的 ghost 锚点不参与可碰撞线段所以一条 4 点开放链条只产生 1 条线段。链条的线段可通过chain.segments获取。⚠️ 链条从双向变为单向是“编译能过、运行才炸”的典型body 会直接穿地而过。由于 Flame 的 y 轴向下屏幕上的绕行顺序与 Box2D 官方文档相反——地面链条应从左到右列出环路在屏幕上顺时针绕行。若 body 穿过链条把点序反转即可。此外对于需要从所有方向阻挡的实体关卡几何如斜坡、可被从下方抵达的平台应改用Polygon链条环是空心的body 一旦越过一条边就会被困在内部。接触监听器变成轮询事件ContactListener与world.setContactListener已被移除。现在每步step之后从 world 上轮询该步发生的事件且每个 shape 必须显式选择opt in要生成的事件// 迁移前 class MyListener extends ContactListener { override void beginContact(Contact contact) { ... } } world.setContactListener(MyListener()); // 迁移后 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 }事件流全貌world.contactEvents包含begin、end、hit三个列表。begin 事件携带接触法线与接触点取代了旧的Manifold。world.sensorEvents包含begin与end的传感器重叠事件每个事件含sensor与visitor两个 shape。传感器与来访者双方都需要ShapeDef.enableSensorEvents。world.bodyMoveEvents报告本步发生移动的 body。end 事件可能引用已被销毁的 shape使用前务必检查Shape.isValid。其他回调的替代preSolve→ 世界级回调world.preSolveCallback返回布尔值决定本步是否求解该接触要求相关 shape 设置ShapeDef.enablePreSolveEvents。postSolve与ContactImpulse不存在了要测碰撞冲击强度开启ShapeDef.enableHitEvents并读取world.contactEvents.hit事件携带point、normal、approachSpeed。自定义碰撞对过滤原来通过子类化 contact filter→world.customFilterCallback。旧Contact类整体消失其方法无直接替代contact.isTouching()不再需要begin 事件本身就表示开始接触contact.getWorldManifold(...)由 begin 事件上的normal与points取代。在 Flame 桥接层ContactCallbacksmixin 的形状保持不变beginContact/endContact大体可继续使用但Contact变成了 flame_forge2d 自己的小类携带shapeA、shapeB、bodyA、bodyB、isSensorEventbegin 事件还有normal、points且contact.fixtureA/fixtureB改为contact.shapeA/shapeB。对应源码可参考 contact.dartend 事件的构造器注释明确写道“shapes may already have been destroyed, checkShape.isValid”并提供contact.isValid便捷判断preSolve/postSolve已从ContactCallbacks移除。另外注意一个 body 在接触期间被销毁不再产生对应的endContact因为路由事件所需的 userData 随 body 一起被清除了。事件 opt-in 的自动处理body_component.dart 的默认createBody()实现里当bodyDef或ShapeDef的userData是ContactCallbacks时会自动为通过shapeSpecs创建的 shape 打开enableContactEvents与enableSensorEvents两个开关。但如果你重写了createBody()就必须自己设置这些标志否则接触回调永远不触发。查询改为直接返回结果射线与 AABB 查询的 callback 类被World上直接返回结果的方法取代。注意射线现在用“起点 平移量”表示而不是两个点// 迁移前 class MyCallback extends RayCastCallback { override double reportFixture( Fixture fixture, Vector2 point, Vector2 normal, double fraction, ) { ... } } world.raycast(MyCallback(), start, end); // 迁移后 final hit world.castRayClosest(start, end - start); final allHits world.castRayAll(start, end - start); world.castRay(start, end - start, (hit) 1);每个RayHit携带shape、point、normal、fraction。world.queryAABB(callback, aabb)→world.overlapAabb(aabb)返回重叠的 shape 列表。包围盒类由AABB更名为Aabb。world.clearForces()已移除——Box2D v3 中作用力按步结算。爆炸效果通过world.explode(ExplosionDef(...))提供。在Forge2DWorld中这些方法以转发形式暴露forge2d_world.dartcastRayClosest/castRay/castRayAll均为 origin translation 语义与overlapAabb且都支持可选的QueryFiltercastRay的回调返回值语义为-1忽略该命中、0停止、命中的fraction将射线裁剪到该命中、1继续且不裁剪。关节类型化工厂方法 本地锚点关节通过 world 上的类型化方法创建并在关节自身上销毁。def 上的initialize辅助方法已移除锚点以本地点形式给出可用body.localPoint(worldAnchor)换算// 迁移前 final jointDef RevoluteJointDef()..initialize(bodyA, bodyB, anchor); final joint RevoluteJoint(jointDef); world.createJoint(joint); world.destroyJoint(joint); // 迁移后 final joint world.createRevoluteJoint( RevoluteJointDef( bodyA: bodyA, bodyB: bodyB, localAnchorA: bodyA.localPoint(anchor), localAnchorB: bodyB.localPoint(anchor), ), ); joint.destroy();可用关节distance、filter、motor、mouse、prismatic、revolute、weld、wheel共 8 种。Box2D v3 中不存在gear、pulley、rope、friction、constant-volume关节。FilterJoint仅用于禁用两个 body 之间的碰撞与WheelJoint是新增的。弹簧参数改名为hertz原frequencyHz且弹簧一般需显式enableSpring开启。关节访问器改为 getter/setterjoint.motorSpeed 2、joint.anglelimit setter 采用命名参数joint.setLimits(lower: 0, upper: pi)。世界空间锚点joint.anchorA/joint.anchorB已不存在只剩本地锚点。需要世界位置时自行计算例如渲染关节final anchorA joint.bodyA.worldPoint(joint.localAnchorA); final anchorB joint.bodyB.worldPoint(joint.localAnchorB);在 flame_forge2d 中旧Forge2DWorld上的createJoint/destroyJoint辅助方法被移除统一改用world.physicsWorld.createRevoluteJoint(def)joint.destroy()。World 与 Body 的变化清单步进与迭代world.stepDt(dt)→world.step(dt, subStepCount: 4)。原来的速度迭代数与位置迭代数合并为单个subStepCount默认 4。对应到 Flame 侧Forge2DWorld.subStepCountforge2d_world.dart默认也是 4update(dt)内先physicsWorld.step(dt, subStepCount: subStepCount)再派发接触事件contactEventsDispatcher.dispatch(...)。body 列表world.bodies不存在了。自己跟踪创建的 body或用world.bodyMoveEvents。Flame 侧Forge2DWorld提供了自己的bodies集合forge2d_world.dart只跟踪经由world.createBody创建的 body且会自动剔除已失效的句柄——直接调用Body.destroy()销毁的 body 也会被清扫避免“Box2D 复用已销毁 body 的槽位导致陈旧句柄读写到别的 body”的隐患。句柄模型与显式销毁World、Body、Shape、Chain及各类关节都是廉价的、值类似的句柄指向原生引擎中的 id。必须显式调用destroy()释放当句柄可能指向已销毁对象时用isValid校验。world.destroy()释放整个模拟。在 Flame 侧Forge2DWorld不会自动销毁物理世界以便 world 移除后可重新加回组件树但你确定不再使用时必须自己调用world.physicsWorld.destroy()若无限不释放Box2D 对同时存在的 world 数量有限制创建时会抛StateErrorforge2d_world.dart。旋转表示Rot旋转现在用Rot余弦/正弦对表示BodyDef(angle: a)→BodyDef(rotation: Rot.fromAngle(a))body.setTransform(position, rotation)接收Rotbody.angle仍然存在。Body 属性重命名迁移前迁移后worldCenterworldCenterOfMassgetLocalCenter()localCenterOfMasssetAwake(value)isAwake valuegetInertia()rotationalInertiabodyTypetyperesetMassData()applyMassFromShapes()setMassData(data)massData dataworldVector(v)rotation.rotate(v)localVector(v)rotation.inverseRotate(v)BodyDef 重命名allowSleep→enableSleepbullet→isBulletactive→isEnabled。快速移动的弹体记得设isBullet true避免隧穿问题。每 body 重力gravityScale从Vector2改为double且它是世界重力的乘数——在零重力世界中它不会产生任何效果。gravityOverride是旧 Dart 移植版的扩展Box2D v3 没有对应物。要给某个 body 独立的重力向量可设gravityScale: 0或保持世界重力为零然后在每次 update 中自行施加作用力例如在BodyComponent中override void update(double dt) { super.update(dt); body.applyForce(customGravity * body.mass); }由于作用力在每步之后都会被清除这必须每次 update 都施加而不是只做一次。若希望静止的 body 保持休眠传入wake: false——这正是常规重力的行为方式。userData 存于 Dart 侧userData现在存在 Dart 侧world 内部而不是原生指针当所属句柄被销毁时userData 一并清除。迁移 checklist 总结升级依赖forge2d: ^0.15.1Flame 侧flame_forge2d: ^0.20.0确认 Dart ≥ 3.12 / Flutter ≥ 3.44并按平台准备 C 工具链。启动阶段或测试 setUpawait initializeForge2D()重写onLoad时先await super.onLoad()。把所有FixtureDefcreateFixture改写为ShapeGeometryShapeDefcreateShape摩擦/弹性放入SurfaceMaterial注意旧默认摩擦 0 需显式SurfaceMaterial(friction: 0)。按形状对照表迁移 Circle/Segment/Polygon/Chain检查链条点序单向、至少 4 点、ghost 锚点语义必要时换Polygon。把ContactListener改为轮询world.contactEvents/sensorEvents/bodyMoveEvents并为 shape 显式设置enableContactEvents/enableSensorEvents重写createBody()的 Flame 用户需自行补开关。把raycast/queryAABB回调类改写为castRayClosest/castRay/castRayAll/overlapAabb射线为 origin translationAABB→Aabb。关节改用类型化工厂方法 joint.destroy()锚点用body.localPoint换算弹簧参数frequencyHz→hertz访问器改 getter/setter。应用 World/Body 重命名表旋转改用RotgravityScale改为 double 乘数自定义重力改手动applyForce。关注世界尺度不要再用“小于 1 米”的布局详见下文并记得显式destroy()原生句柄。附为什么世界尺度问题最值得警惕flame_forge2d 的迁移文档将“世界尺度”列为最可能让一个能编译能运行的游戏坏掉的变化Box2D v2 将每个 body 限制在每步 2 米约 120 m/s旧文档因此建议把世界布局得远小于 1 米Box2D v3 改为WorldDef.maximumLinearSpeed默认 400 m/s可逐 world 设置并引入 speculative contacts——两个形状相距Tolerances.speculativeDistance0.02 米内即报告接触。结果是刻意做成亚米尺度的世界会出现“隔空报接触、永不反弹、body 还在动就被催眠”等怪象。因此迁移时应优先把世界放大到移动 body 约为 0.110 米长度与重力乘以同一系数 S时间不受影响再把metersToPixels除以 S 以保持屏幕尺寸不变各物理量的缩放关系见 forge2d.md 的尺度章节。若无法改布局可向Forge2DGame构造函数传入lengthUnitsPerMeter例如人物高 0.04 个单位就传super(lengthUnitsPerMeter: 0.04)Box2D 的绝对长度容差会随之缩放该设置是进程级、不可在物理世界创建后修改的同一时刻运行的游戏必须一致否则抛StateError。源码层面Forge2DGame.lengthUnitsPerMeter正是被转发给initializeForge2D的forge2d_game.dart。调试模式下 flame_forge2d 在创建过小的移动 body 时会打印一次警告。相关文档与源码入口Forge2D 迁移指南本文主文档flame_forge2d 迁移指南桥接层Forge2D 入门与初始化flame_forge2d 总览Forge2DGame / Forge2DWorld / BodyComponent / 单位与尺度flame_forge2d 关节文档源码Forge2DGame、Forge2DWorld、BodyComponent 与 ShapeSpec、Contact 事件类型、Forge2DViewfinder 与 metersToPixels、依赖与 SDK 约束【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表