Skip to main content

just_physics_engine 1.4.0 copy "just_physics_engine: ^1.4.0" to clipboard
just_physics_engine: ^1.4.0 copied to clipboard

Standalone 2D/3D physics engine for Flutter: 2D on native Box2D v3.2 (FFI) with a pure-Dart fallback, 3D on Jolt Physics through just_jolt.

1.4.0 - 2026-10-15 #

3D physics on Jolt, a 2D character mover, and native Box2D on every native platform — Jolt and Box2D each in a package of their own, just_jolt and the new just_box_2d.

Breaking changes #

  • PhysicsEngine3D is now the 3D engine interface; the experimental placeholder class of that name (initialize, update, dispose) is gone.
  • CollisionShape.area is a new abstract getter: a shape class written outside this package must implement it.
  • Needs Flutter 3.47.0 or later. just_box_2d's and just_jolt's build hooks use native_toolchain_c 0.19.3, which needs meta ^1.19.0, and every Flutter before 3.47 pins meta 1.18 or older, so an app on one does not resolve.
  • Native Box2D moved to the new just_box_2d package (^0.1.0). This package compiles no C and has no build hook of its own; just_box_2d's hook needs hooks ^2 (with code_assets >=1.2.0 <3.0.0 and native_toolchain_c ^0.19.3), so an app that pins hooks ^1 elsewhere will not resolve. It builds by default; hooks: user_defines: just_box_2d: build: false in an app's pubspec turns it off, and every platform runs the pure-Dart engine.
  • Native Box2D now runs on Android, iOS, macOS and Linux, which all silently ran the pure-Dart fallback before (see Changed): physics there now behaves as Box2D does. Android was checked on an emulator; iOS, macOS and Linux were not run.
  • The pure-Dart backend behaves as Box2D does in three places: friction mixes as the geometric mean, nothing bounces below 1 m/s, and a kinematic body no longer touches static or kinematic ones (see Changed).
  • Every segment of an open ChainShape is solid on the native backend too. Box2D treats an open chain's first and last points as ghosts, so the end segments had no collision there (and fewer than 4 points none at all); the wrapper now adds ghost points beyond the ends, as the pure-Dart engine already behaved.
  • Box2DPhysicsEngine.createMouseJoint, Box2DJoint.setTarget and Box2DJointFactory.createMouse are gone: Box2D v3 has no mouse joint, and they always returned null or did nothing. addMouseJoint gives the pure-Dart MouseJoint on every backend.
  • Box2DJoint.setDistanceSpring and setWheelSpring take hertz and dampingRatio, which is what Box2D always read them as.
  • Pure-Dart sliders and wheels count from where they were made, as Box2D's do: a PrismaticJoint holds bodyB across its axis where it started (it pulled it onto a line through bodyA's centre) and its limits are travel from there; a WheelJoint's spring rests where the wheel started (it pulled the wheel into bodyA's centre). Both axes turn with bodyA, and are made unit length. A RevoluteJoint's limits act on the angle from what it was when made (jointAngle).
  • pollSensorBeginEvents and pollSensorEndEvents give the sensor first on the pure-Dart engine too, as the native one always has.
  • Lines are as thick, and as solid, on both backends. Natively a segment, and each segment of a chain, is now the capsule its thickness makes (Box2D got a bare line), and a chain is solid from both sides (a Box2D chain is solid on one): a body resting on a line sits thickness higher natively than it did.
  • Queries see every shape of a body, turned with it, on the pure-Dart engine too (castRay, castRayAll, castCircle, queryAABB, queryCircle), and a ray passes the shape it starts inside on both backends (the pure-Dart engine reported the far side).
  • SpatialGrid.cells is private.
  • package:just_physics_engine/just_physics_engine.dart exports the 3D types (…3D, JoltPhysicsEngine), and physics_3d.dart re-exports just_dart's Vector3 and Quaternion: hide them where another package's names clash.

Added #

  • 3D physics on Jolt Physics (v5.6.0, through the new just_jolt package). PhysicsEngineFactory.create3D() gives a JoltPhysicsEngine, a PhysicsEngine3D making PhysicsWorld3Ds: static, kinematic and dynamic bodies of box, sphere, capsule, cylinder, convex hull, mesh, height-field and compound shapes; forces, impulses and kinematic moves; batched pose reads; buffered contact, sensor and activation events; raycasts, shape casts and overlaps; characters that climb steps and report the ground they stand on (its velocity, for riding a platform); fixed, point, hinge, slider, distance and six-DOF joints with motors. Contacts mix bounce as the 2D engines do (the less bouncy body). Units are metres and +Y is down. Also on its own in package:just_physics_engine/physics_3d.dart; JoltWorld and JoltShapes, native only, in package:just_physics_engine/jolt.dart.
  • Jolt is built only when the app's pubspec asks (hooks: user_defines: just_jolt: build: true), and the 3D code is reachable only through create3D, so a 2D app carries none of it. Where 3D cannot run (the web, 32-bit Android, Jolt not built) create3D throws Physics3DUnsupportedError; PhysicsEngineFactory.supports3D and unavailable3DReason ask first (and start Jolt with its default job system where it can run).
  • Shape3D.isValid: whether a backend can make a shape. Neither backend makes a body of one that is not.
  • FakePhysicsEngine3D in package:just_physics_engine/testing.dart: a deterministic plain-Dart 3D engine for tests, keeping Jolt's rules for handles, shapes and which pairs touch.
  • A character mover (CharacterMover2D): moves a capsule or a box (MoverShape) through the world as a kinematic character — Box2D's mover, after Quake's — sliding along walls and slopes, stopping at ceilings, climbing steps up to a height, keeping to the floor going down, standing on one-way platforms and jumping up through them, and pushing dynamic bodies. A slope too steep to stand on acts as a wall to walk into and still slides it down. move reports where it ended up, its velocity, and what it stands on (GroundState2D, the ground's normal and body); carrying it with a moving platform is the caller's.
  • The two world queries it is built on, on both backends: PhysicsEngine.collideMover (the surfaces a mover touches, as MoverPlanes) and PhysicsEngine.castMover (how far it can move), with a MoverFilter (layers, never sensors, never its own body). Native Box2D answers them in C (b2w_moverCollide, b2w_moverCast, on Box2D's own GJK and shape cast); the pure-Dart engine with the same rules in Dart, so a character walks the same on the web. MoverSolver.solvePlanes and clipVector are Box2D's plane solver.
  • ShapeProxy2D, ShapeDistance2D.between and castShape2D: Box2D's GJK distance and conservative-advancement shape cast, in Dart.
  • PhysicsEngine.mixFriction, mixRestitution and restitutionThreshold: how two bodies' materials combine at a contact, on both backends.
  • PhysicsEngine.bodyCount.
  • CollisionShape.area: how much ground a shape covers (0 for a segment or a chain), for weighing a body by its shape.
  • The web's Box2D classes have the whole native API (the joint creators, destroyJoint, registerImpactCallback, physicsBodyFromHandle, Box2DBody.ofType, …), doing nothing there, so code written against them compiles for the web. Box2DBody's destroy and force methods on the web now do nothing instead of throwing.
  • Box2D comes from just_box_2d: Box2D v3.2 and the b2w_* C layer, built from source by its own hook, usable without this package.
  • JointConstraint.bodies, PrismaticJoint.translation and RevoluteJoint.jointAngle.
  • Box2DPhysicsEngine.addPrismaticJoint makes a native slider; it made a Dart one the native step never applied.

Changed #

  • "Physics Engine initialized" and "… disposed" print only in debug builds.
  • Friction combines as Box2D combines it on the pure-Dart backend too: the geometric mean of the two bodies' friction, not their average, so ice stays slippery on any surface and the web plays as native does.
  • No bounce below 1 m/s (100 units/s) on the pure-Dart backend, Box2D's restitution threshold: slow contacts settle instead of hopping for ever. Restitution still combines as the less bouncy of the two on both backends.
  • A kinematic body no longer touches a static one, or another kinematic one, on the pure-Dart backend — Box2D makes no contact unless one side is dynamic. A moving platform sliding past a wall raised contact events on the web only. Sensors still see every kind of body.
  • Native Box2D on Android, iOS, macOS and Linux. The bindings are now @Native functions (ffigen ffi-native) that find the library through the code asset just_box_2d's build hook bundles (package:just_box_2d/just_box_2d), instead of DynamicLibrary.open('box2d_flutter'). That name only ever worked on Windows (Android's file is libbox2d_flutter.so, Apple's and Linux's are named differently again), so games elsewhere silently ran the pure-Dart fallback. just_box_2d's Box2D.ensureAvailable() is the availability probe (it resolves the bundled library and the newer functions up front), and the pure-Dart fallback is unchanged where the library is missing (the web, or an app that turned the build off). Internal: Box2DBindings is gone.
  • The Android and Linux library links libm. Without it the library failed to load on Android ("cannot locate symbol atan2f"), the second reason Android never ran native Box2D. Checked on an Android x86_64 emulator: the probe finds the library and the platformer parity tests pass natively.
  • Box2D lives in the new just_box_2d package. Box2D v3.2 and the b2w_* C layer, with a build hook on hooks ^2, code_assets >=1.2.0 <3.0.0 and native_toolchain_c ^0.19.3 (the same generation as just_graphics_engine, so both resolve in one app), moved out: this package is plain Dart, ships no C and needs no git submodule, and other code can use Box2D through just_box_2d alone. native_toolchain_c 0.19.3 runs the compiler directly instead of through cmd.exe, so long source lists no longer hit the Windows command-line limit.
  • The native Box2D classes are what the analyzer and the API docs see. The library exports them by default and swaps in the web's stubs only where dart.library.js_interop is (JS and Wasm), so an IDE no longer reports native-only calls such as registerImpactCallback as undefined.
  • A PolygonShape of more than 8 vertices becomes several Box2D polygons on the native backend, so it keeps its whole outline (Box2D's polygons hold at most 8).
  • The published package ships no C code, and no stray coverage report.
  • registerImpactCallback reports Box2D's hit events: every contact that closed faster than the threshold in a step, with the speed it closed at. It used to compare the two bodies' velocities after the step, and only for contacts that had just begun.
  • The bundled Box2D is v3.2; the docs said v3.0.
  • Use Box2D and Jolt from one isolate per process: their native worlds are shared by the whole process.
  • The pure-Dart joints hold. Each takes away the relative velocity it forbids as well as correcting position, so gravity no longer stretches a rope or a rod without end (a 100-unit rope reached 409 units in ten seconds); a body hinged away from its centre swings round the anchor, drawn turning (it hung as if welded); joints move dynamic bodies only (a kinematic platform holding a lamp sank). A distance spring is soft, with no hard stop, and its damping slows it — the damping pushed the wrong way, adding energy.
  • castCircle is Box2D's shape cast against every shape; it cast a ray from the circle's centre at polygons, segments and chains, ignoring the radius.
  • Box2DPhysicsEngine brings a native body's type, gravity scale and mass up to date whenever they change, set through the engine or written on the PhysicsBody, and every native shape has density 1, so a body that turns dynamic has its shapes' mass and inertia.
  • A body under gravity never sleeps on the pure-Dart engine — it has no islands to wake a body whose support moves away — and PhysicsBody.canSleep now says so.

Fixed #

  • A hot restart no longer crashes the app. An isolate that shuts down without disposing its engine has every world and body still attached to a NativeFinalizer finalized at once, in no set order — so a world could go before its own bodies, and each body's finalizer then destroyed a body inside a wiped world. Destroying a body, joint or chain does nothing for a handle that is no longer valid, and b2w_step does nothing for a world that has gone.
  • A finalizer never touches Box2D from another thread. The GC can run a finalizer on any thread, even while the isolate is inside a step or any other call, and Box2D is not thread-safe. A finalizer now only queues the destroy, and the next call from Dart performs it; the impact callback runs with no lock held.
  • A stale native handle never names a live body. Box2D checks an id against its world's slot only, and a world made in a freed slot numbers its bodies from the start again, so a body of a disposed world (destroyed, or finalized, later) could destroy or change a body of the new one. Body, chain and joint handles now carry their world's generation, and every native entry point checks its handles before Box2D sees them — Box2D itself checks nothing in a release build — and does nothing with a dead one.
  • Creating a body in a disposed world gives 0 (a Box2DBody made that way throws), and so does a 129th world (Box2D's limit is 128) instead of writing out of bounds.
  • A joint between bodies of another world, or between a body and itself, is not made; Box2D found the bodies by index alone and read the wrong world's.
  • Changing a body's friction, bounciness, filter, one-way side or sensor flag reaches every one of its shapes; a body of more than 64 (a long chain makes one per segment) kept the old settings on the rest.
  • A chain of 2 or 3 points no longer reads or allocates out of bounds on the native backend, and a chain's points closer together than Box2D's linear slop are merged (a segment of no length has no normal).
  • Contact and sensor events for a shape destroyed before they were read name no body, instead of whichever body took its slot.
  • A new body never inherits the sensor flag of an earlier body that had the same handle.
  • Box2DPhysicsEngine.dispose clears the native impact callback before it closes the Dart callable it points at.
  • A native distance joint's spring works. addDistanceJoint with a stiffness set the spring's frequency and damping but never switched it on, so the joint stayed rigid, and a range between minLength and maxLength held the bodies rigidly at its middle, as its limit was off.
  • Native contact, sensor and body-move events are each update's own. Box2D keeps only the last step's events: a frame that ran two steps lost the first one's (a landing missed, and no jump; a sensor exit missed, and "in water" for ever), and a frame that ran none reported the last frame's again (double damage, double pickups) — below 60 fps and above it.
  • Dart joints act on the native backend. addMouseJoint (it added two mouse joints, and neither did anything there) and a joint made for a body the native engine does not hold now reach the native body through its force and velocity. removeJoint destroys a native joint, so its bodies come apart (they stayed hinged); removeBody takes the body's joints out of joints. addWheelJoint turns the wheel about its own centre natively; it turned it about the midpoint, and the wheel wobbled.
  • Forces act on the native backend as the pure-Dart engine integrates them: a frame's applyForce goes in as the change of velocity it makes (an impulse of force × step) before the step moves the body, and waits for a frame that steps. Box2D held it across its sub-steps, which adds energy to anything spring-like built on forces (the mouse joint swung further every time), and a frame with no step gave it twice.
  • A native body turned dynamic from static or kinematic falls, and turns: its gravity scale is set again (a static one made without gravity hung in the air), and it has its shapes' rotational inertia.
  • A native kinematic body never falls asleep; one asleep ignored the velocity it was given.
  • Native castRayAll keeps each body's nearest hit (it kept whichever came first), and it and queryAABB find every body: both stopped at 32 shapes, static ones first, so a body beyond a row of tiles was never found.
  • Native castRay with exclude passes a shape the ray starts inside, as without it; it reported that shape at distance 0 with no normal.
  • A second initialize() on Box2DPhysicsEngine keeps its world; it leaked its buffers and threw every frame. Without the native library debug drawing and alpha are the pure-Dart engine's (drawing was blank), and stats['awakeBodies'] counts awake bodies only.
  • The native backend's previous pose for interpolation is the one before the frame's steps; a frame that ran none made it the current one.
  • Pure-Dart contact normals point from A to B wherever a polygon sits on its body. They were judged from the bodies' origins, so a polygon away from its own (a collider offset, a compound body, terrain in world coordinates) pushed bodies into itself, and crates fell through it.
  • Pure-Dart capsules collide with segments (one added before a segment fell through it), and a rounded polygon's corner radius counts against capsules, segments and other rounded polygons.
  • A pure-Dart polygon with no edge length collides with nothing; it turned bodies to NaN and stopped the world.
  • Removing a pure-Dart body ends its contacts without events (they named the removed body), takes its joints and the record kept of it; dispose clears contact state too, so an engine used again reports nothing of its last world.
  • A linear impulse on a native body pushes at its centre of mass, as the pure-Dart engine does. It pushed at the body's origin, spinning a body whose shapes sit off it.
  • 3D: a character's body can no longer be destroyed or joined on its own, which crashed Jolt natively (it was destroyed again with its character, or a joint went on holding it). Raycasts and overlaps return characters' bodies, so a "destroy what the blast hits" script could do it. A world now acts only on the bodies, joints and characters it made itself, which also passes over handles from another world (they used to reach into that world).
  • 3D: a BodyPoses3D read before a world's dispose stays readable after it; it used to point at freed native memory.
  • 3D: after destroyAll, a world still held reads as disposed, instead of calling Jolt through freed memory.
  • 3D: a shape Jolt cannot make safely — a mesh index past its vertices, a NaN or zero size, too few hull points, an empty compound — makes no body, instead of reading out of bounds or leaking its native shape.
  • 3D: rotations passed to setBodyPose and moveKinematic are made unit length (one of no length is none), as createBody already did.
  • 3D: a six-DOF joint whose lists are shorter than 6 is not made, instead of throwing; other kinds never read them.
  • 3D: rotations inside a TransformedShape3D are made unit length before Jolt sees them (a compound's part could come out NaN).
  • 3D: MeshShape3D is for static bodies only — Jolt refuses kinematic ones too; the docs said otherwise. Handles can come back once what they named is destroyed (Jolt reuses body ids); the docs said they never did.
  • 3D: a dead character reads as zeros, in the air.
  • FakePhysicsEngine3D follows Jolt where tests notice: meshes on static bodies only, invalid shapes and a full world give 0 (a character in a full world comes without its body), a layer outside 0–15 is ignored (it threw on the next step), a contact needs a dynamic body (or a kinematic body and a sensor), a moving body wakes the sleeping bodies it touches while bodies at rest together sleep together, a turned shape's bounds follow its rotation, a rotation of no length reads as none, kinematic bodies sleep, a character's body moves only with its character, activation events come when asked for, a character on a rising platform is lifted instead of pushed under it, dispose drops pending events and step(0) does nothing. JoltWorld.step(0) does nothing too.
  • FakePhysicsEngine3D sleeps as Jolt does: a moving kinematic sensor wakes nothing, a sensor is never woken by what touches it, and a pair asleep together reports no contactStay.
  • 3D: JoltPhysicsEngine.joltVersion, liveWorlds and destroyAll start Jolt with the engine's workerThreads; they started it with the default.
  • The character mover: it steps up only while it still stands after the move, as Jolt's character walks stairs, so stepping no longer cancels a jump; the pure-Dart mover no longer walks through a turned body (its broad phase ignored body rotation); a mover partway through a one-way platform is no longer caught by it on the way through (it crept, then dropped); an up of any length is made unit length; a MoverPlane.pushLimit below 0 counts as 0 instead of throwing.

Removed #

  • The experimental PhysicsEngine3D placeholder class (initialize, update, dispose); the name is now the 3D engine interface.

1.3.0 - 2026-10-04 #

Cross-platform parity release. Closes the gaps where the native Box2D backend silently behaved differently from the pure-Dart one — all of which a 2D platformer runs straight into.

Added #

  • PhysicsEngine.setBodyTransform — teleport a body. Previously writing PhysicsBody.position worked on web but was silently discarded on native (there was no b2w_setTransform), so respawns, checkpoints, level-load placement and warps did nothing on desktop and mobile.
  • BodyType { static, kinematic, dynamic } and PhysicsBody.bodyType. Kinematic bodies are moved only by explicit velocity writes and cannot be pushed — moving platforms, elevators and crushers. mass <= 0 still forces static via effectiveBodyType, so nothing that predates this shifts.
  • PhysicsBody.gravityScale — a real per-body float, where only the boolean useGravity existed before. Read effectiveGravityScale, which folds both together. This is what variable jump height is built from.
  • One-way platforms on the native backend. isOneWay was honoured only by the pure-Dart resolver. There is now a Box2D pre-solve contact filter with matching semantics, plus PhysicsBody.oneWayDirection to pick which side is solid.
  • Native ray casting and AABB queries. castRay, castRayAll and queryAABB now route to Box2D's BVH instead of the inherited brute-force Dart scan, which also ignored body rotation and compound shapes. Out-param buffers are allocated once per engine, not per cast.
  • Runtime body mutation: setBodyType, setBodyGravityScale, setBodyFilter, setBodyOneWay, applyLinearImpulse, setBodyDamping, setBodyFriction, setBodyRestitution. Several of the underlying native symbols already existed but were only ever called at body-creation time.
  • CircleShape(radius, center: ...) — a circle off its body's centre: a character's feet, a wheel hung below a cart. Both backends collide, cast rays and draw it there; the native one also turns it with the body (the pure-Dart backend does not turn shapes).
  • PhysicsBody.angularDamping — spin decay per second, as drag is for velocity; 0 by default. PhysicsEngine.setBodyAngularDamping.
  • PhysicsBody.canSleep — false keeps a body awake at rest. PhysicsEngine.setBodyCanSleep, which wakes the body when it forbids sleep.
  • PhysicsEngine.setBodyBullet — continuous collision switched at runtime; before, only at creation.
  • New C wrapper symbols: b2w_createBody, b2w_setBodyTransform, b2w_setBodyType, b2w_getBodyType, b2w_setBodyTargetTransform, b2w_setAngularVelocity, b2w_setBodyOneWay, b2w_castRayClosest, b2w_castRayAll, b2w_queryAABB, b2w_setBodyLinearDamping, b2w_setBodyAngularDamping, b2w_setBodyFriction, b2w_setBodyRestitution, b2w_setBodyFilter64, b2w_setBodySleepEnabled.

Changed #

  • The pure-Dart backend now runs a fixed 1/60 s timestep with a real alpha remainder, matching what the Box2D backend always did. Web physics was previously framerate-dependent: the same jump reached a different height at 60 Hz and 144 Hz. Opt out with PhysicsEngine.pureDart(fixedTimestep: false). This changes existing pure-Dart behaviour — drag compounds at a fixed rate and sleep timers tick differently.
  • One-way platforms are now decided from the contact normal rather than the mover's velocity, so a body that stalls at the apex of a jump inside a platform still passes through. A pass-through latch holds until the bodies separate, so a body rising through a platform is no longer caught on the way out when the normal flips.
  • loadBox2DLibrary() eagerly resolves the newly added symbols. Bindings are looked up lazily, so a stale box2d_flutter.dll used to report a healthy 'box2d_v3' backend and then throw "Failed to lookup symbol" mid-game, on the first respawn. It now fails during initialize() and falls back cleanly to pure Dart with an honest 'dart_fallback'.
  • b2w_createDynamicBody / b2w_createStaticBody now forward to b2w_createBody; their signatures are unchanged.
  • The pure-Dart backend's drag no longer slows spin; angularDamping does, as on Box2D. A body that relied on drag to stop spinning on the web sets angularDamping instead.
  • The pure-Dart backend skips pairs of static bodies, as Box2D does. They cannot move, so they can neither push each other nor start or stop touching; a level built from hundreds of static pieces (a tile map's collision) no longer tests every neighbouring pair each step, and pieces of the same floor no longer report contacts or sensor events between themselves.
  • Native ABI (only this package's own bindings call it): b2w_addCircleShape takes the centre after the body handle; b2w_bulkExtractTransforms writes 7 floats per body (the 7th the angular velocity).

Fixed #

  • Contact events never fired on the native backend. b2DefaultShapeDef() zero-initialises, so enableContactEvents defaulted to false and the wrapper never set it. Collisions resolved correctly — bodies landed and stacked as expected — but pollContactBeginEvents and pollContactEndEvents stayed permanently silent, while the pure-Dart backend generated events normally. Anything built on contacts, including PhysicsBodyComponent.isGrounded and therefore every jump in a platformer, worked on web and silently did nothing on desktop and mobile.
  • Kinematic bodies no longer free-fall. Box2D gates gravity on inverse mass rather than body type (gravityScale = invMass > 0 ? gravityScale : 0), so the existing unconditional mass override gave kinematic bodies a non-zero inverse mass and gravity applied. Mass and shape density are now overridden for dynamic bodies only.
  • applyLinearImpulse on the native backend mirrors the resulting velocity back into the Dart body. update() pushes body.velocity into native every frame, so an impulse applied between frames was integrated by Box2D and then overwritten by the stale Dart value on the very next push.
  • setBodyTransform writes both the previous and current interpolation snapshots, so a teleport no longer renders as a streak from the old position to the new one.
  • Per-body linear damping now reaches the native backend; PhysicsBody.drag was pure-Dart only, so air drag existed on web and not on device.
  • Spin on the native backend. PhysicsBody.angularVelocity was never read back from Box2D nor written to it: a body spun by a collision read 0, and one given a spin started still. Both directions now sync, the write only when something changed the value.
  • A solid body could come out a sensor on the native backend. The wrapper keeps each body's sensor flag by handle, handles come back (a new world, or a reused slot), and addBody wrote the flag only for sensors. A solid body given a dead sensor's handle let everything through. addBody now writes the flag for every body, and destroying a body forgets its flag.
  • Heavy damping reversed a body on the pure-Dart backend. Each step scaled velocity by 1 - drag * dt, which goes negative once drag * dt > 1 (a drag of 100 at 60 fps): the body flipped direction instead of stopping. The factor, and the new spin one, now stop at 0.

Known limitations #

  • setBodySensor remains create-time-only on the native backend. Box2D forbids converting a shape between sensor and solid at runtime because it breaks the sensor begin/end event contract; the call now logs a warning instead of silently doing nothing. Gate a trigger with isActive or a collision filter.
  • The pure-Dart narrow phase still ignores body rotation entirely, so on web a rotated body collides as its unrotated shape. Use fixedRotation: true for characters.
  • One-way platforms can differ slightly between backends on sloped geometry: this Box2D fork's pre-solve callback receives only (point, normal), with no contact separation, so the normal threshold is the only signal available.

Dependencies #

  • just_dart: ^0.2.0 (was ^0.1.0).

Tests #

  • New test/platformer_parity_test.dart: fixed-timestep frame-rate independence, gravityScale, kinematic bodies, one-way platforms, runtime mutation, ray casting, and contact/sensor events — asserted against the pure-Dart backend and replayed against the native one, skipping rather than failing when the Box2D submodule has not been built.
  • New test/static_pairs_test.dart: overlapping static bodies raise no contacts or sensor events, and a moving body still lands on them.
  • New test/body_settings_test.dart: an off-centre circle, angular damping against drag, canSleep, and continuous collision switched at runtime.

1.2.2 - 2026-08-02 #

Correctness and WASM-compatibility patch release.

Fixed #

  • PhysicsEngineFactory (physics_engine_factory.dart) still used the old dart.library.html conditional import that 1.2.1 removed everywhere else. On WASM web builds, where dart.library.html is not defined, this resolved to the native Box2D FFI backend instead of the pure-Dart fallback, pulling dart:ffi into the web/wasm compile graph and breaking the build. All conditional imports now consistently key off dart.library.io.
  • Native contact-begin events (b2w_getContactBeginEvent) always reported a (0, 0) contact normal instead of the real collision normal. Now reads it from b2Contact_GetData.
  • Box2DJoint.reactionForce always returned Offset.zero even though the underlying native query (b2w_getJointReactionForce) was fully implemented and bound; it just wasn't being called.
  • Box2DPhysicsEngine.dispose() destroyed native bodies but never called destroy() on native joints before tearing down the world, leaving any retained Box2DJoint in a stale, not-_destroyed state.
  • RevoluteJoint (pure-Dart) accepted limitEnabled/lowerLimit/upperLimit but never enforced them — angle limits are now applied during applyConstraint.
  • ChainShape was not recognized as a valid collision partner by CircleShape, PolygonShape, CapsuleShape, and RoundedPolygonShape — collisions against chain terrain were silently missed depending on which body was added to the engine first. All four now delegate correctly.
  • A plain PolygonShape colliding against a RoundedPolygonShape ignored the other shape's cornerRadius when the plain polygon was the "self" side of the check.

Native Box2D backend correctness

Found while verifying the fixes above with the full test suite — the native backend was silently producing wrong physics on every device, not just a test artifact:

  • Every fast-moving body was speed-capped far too low. b2w_createWorld never called b2SetLengthUnitsPerMeter, so Box2D's tuning constants (maximumLinearSpeed, sleep threshold, etc.) used their meters-scale defaults against this package's centimetre-scale convention (gravityY = 981). Box2D's "faster than the speed of sound" 400 m/s safety cap resolved to 400 centimetres/s — 4 m/s — silently clamping any reasonably fast falling or thrown body. Now set once to 100 (1 m = 100 of this package's units), matching the pure-Dart engine's own tuning (e.g. PhysicsBody.sleepVelocityThreshold's default of 5.0 now lines up with Box2D's resulting 5 cm/s default sleep threshold — not a coincidence).
  • PhysicsBody.useGravity = false had no effect on the native backend — nothing set the native body's gravityScale. Added b2w_setBodyGravityScale, called from addBody().
  • PhysicsBody.isAwake was never synchronized with native Box2D, in either direction: a body created with isAwake: false actually started awake natively, and nothing ever wrote Box2D's real sleep state back to the Dart field, so it stayed stuck at whatever the caller last set. Added b2w_setBodyAwake (pushed once at creation) and extended b2w_bulkExtractTransforms's previously-unused padding float to report awake state every step.
  • PhysicsBody.applyForce()/applyTorque() had no effect on the native backend — they only wrote to the Dart-only acceleration/torque fields, which nothing read on this backend. Box2DPhysicsEngine.update() now pushes them through b2w_applyForce/b2w_applyTorque each frame and resets them afterward, matching the pure-Dart engine's per-frame consumption.
  • A dynamic body's actual simulated mass never matched PhysicsBody.mass. Box2D derives mass from shape area × density, and this wrapper always passed a fixed density = 1.0, so e.g. a CircleShape(5.0) massed ~78.5 regardless of what PhysicsBody.mass said — silently distorting every force, impulse, and collision response. Added b2w_setBodyMass, which overrides Box2D's shape-derived mass with PhysicsBody.mass (scaling rotational inertia proportionally to keep angular response consistent), called after shape fixtures are attached.
  • The test suite hung/crashed partway through on higher-core-count machines. Each Box2DWorld spun up its own ThreadPool sized to hardware_concurrency - 1 (23 threads on a 24-core machine); creating and disposing dozens of worlds in one process (as the test suite does) exhausted OS thread/handle resources. Capped the auto-detected default to 4 workers — callers needing more can still pass an explicit numThreads.
  • Two tests were themselves written assuming pure-Dart semantics (a single large update(1.0) call, or 20 steps of 0.016s) and don't hold against the Box2D backend's fixed-timestep accumulator (which caps at 5 steps per call to prevent spiral-of-death) or its internal fixed 0.5s time-to-sleep constant. Adjusted both to step in a fixed-timestep-compatible way.
  • The native Box2D backend never actually worked on Windows. box2d_wrapper.h/.cpp declared all b2w_* functions with no export annotation; unlike ELF/Mach-O, Windows DLLs export nothing by default, so link.exe silently produced a DLL with an empty export table. Every dart:ffi DynamicLibrary.lookup() call failed with "procedure not found" even though the .dll itself loaded fine. Added a B2W_EXPORT macro (__declspec(dllexport) on Windows, __attribute__((visibility("default"))) elsewhere) to every native entry point.
  • Restitution mixing disagreed between backends. Box2D 3.0's default restitution mixing is max(a, b); the pure-Dart engine mixes with min(a, b) (the documented behavior — see the Bounciness demo's "Resolution uses: min(a.restitution, b.restitution)"). A perfectly-restitutive floor (restitution = 1.0, relied on so "the ball governs bounce" under min-mixing) instead forced every contact to max(1.0, ball) == 1.0 on the native backend, making every ball bounce identically regardless of its own restitution. Installed a matching min-mixing restitutionCallback on native world creation.
  • Joint motors didn't wake sleeping bodies on the native backend. b2*Joint_EnableMotor/SetMotor* only write into the joint's solver state — they never wake the jointed bodies. A body that had fallen asleep (e.g. a car settled on its suspension) had its island skipped by the solver entirely, so SetMotorSpeed/EnableMotor silently produced no motion until something else disturbed it. Revolute, prismatic, and wheel joint motors now explicitly wake both jointed bodies when enabled.
  • PhysicsBody.velocity writes never reached the native simulation. Nothing bridged Dart → native for velocity, so any ECS-driven velocity change (player input, knockback, etc. — e.g. PhysicsSystem's per-frame push from VelocityComponent) was silently discarded on the Box2D backend, and the very next transform sync overwrote it back to whatever native already had. Box2DPhysicsEngine.update() now pushes each dynamic body's current velocity into the native body before stepping. Likewise, a body's initial velocity set at construction time is now carried over to the native body when added — previously it silently started at rest.

Added #

  • Pure-Dart WheelJoint and unified addWheelJoint() on PhysicsEngine/Box2DPhysicsEngine. Previously createWheelJoint only existed on the native Box2D backend with no Dart fallback, unlike every other joint type — contradicting the 1.2.0 changelog's claim of full pure-Dart joint parity.
  • Solid-contact begin/end event polling (pollContactBeginEvents/pollContactEndEvents) on the pure-Dart PhysicsEngine. Previously only the Box2D backend exposed contact-begin polling, with no pure-Dart equivalent and no contact-end event at all on either backend.
  • One-way / pass-through platform support via PhysicsBody.isOneWay on the pure-Dart engine — contacts against an isOneWay body are skipped while the dynamic side is moving upward, so a body can pass through from below and only lands when moving downward onto it. Pure-Dart only for now; the Box2D FFI backend has no first-class one-way-platform primitive (would need a native PreSolve contact filter) and currently ignores this field.

Changed #

  • The native Box2D submodule now points to just-unknown-dev/just-box-2d (a maintained fork) instead of upstream erincatto/box2d, at src/native/third_party/just_box_2d.

1.2.1 - 2026-06-11 #

Metadata and compatibility patch release focused on clearer pub.dev platform signaling and safer WebAssembly target behavior.

Added #

  • Explicit pub.dev platform declarations in package metadata for Android, iOS, Linux, macOS, Web, and Windows.
  • Pub.dev topic tags to improve discoverability, including wasm.

Changed #

  • Updated conditional backend import routing so only dart.library.io targets resolve to the native Box2D FFI path.
  • Non-IO targets (including WASM and Web) now consistently resolve to the pure-Dart/stub Box2D-compatible surface.
  • README compatibility section now reflects the current package/version constraints and platform/backend support matrix.

Notes #

  • This release does not introduce physics behavior changes; it improves package metadata accuracy and cross-target compatibility guarantees.

1.2.0 - 2026-05-27 #

Feature release focused on richer 2D authoring/query APIs, compound bodies, and broader Box2D parity for joints and sensors.

Added #

  • New 2D collision shapes: CapsuleShape, SegmentShape, ChainShape, and RoundedPolygonShape.
  • Compound-body support through PhysicsBody.additionalShapes, PhysicsBody.isCompound, and aggregate bounds handling for broad-phase collision.
  • World query APIs: castRay(), castRayAll(), castCircle(), queryAABB(), queryCircle(), and queryPoint().
  • New query result types: RayBodyHit and ShapeCastResult.
  • Joint constraint support for the pure-Dart engine: DistanceJoint, MouseJoint, WeldJoint, RevoluteJoint, PrismaticJoint, and WheelJoint via the unified engine API.
  • Native Box2D joint wrappers and configuration helpers through Box2DJoint plus creation APIs for revolute, prismatic, distance, weld, and wheel joints.
  • Sensor begin/end polling across both backends, plus native Box2D sensor event bridging.
  • Expanded collision filtering and body flags with categoryBits, maskBits, groupIndex, and isBullet on PhysicsBody.
  • Comprehensive test coverage for joints, collision primitives, engine lifecycle, and query helpers.

Changed #

  • Spatial grid broad-phase now uses compound bounds so multi-shape bodies are culled correctly.
  • Native Box2D fixture creation now applies sensor state during shape creation and supports rounded polygon, capsule, segment, and chain fixtures.
  • Web-safe Box2D public stubs were expanded to stay import-compatible with the new joint API surface.
  • PhysicsEngine3D is now explicitly marked experimental.

Notes #

  • This release expands the 2D gameplay API significantly while keeping the package centered on a shared pure-Dart and Box2D-backed interface.
  • On the Box2D backend, mouse-joint behavior still falls back to the Dart-side spring constraint because Box2D v3 no longer exposes a native mouse joint.

1.1.0 - 2026-05-20 #

Performance-focused update that streamlines 2D collision detection and simplifies the physics body force API.

Added #

  • Architecture documentation explaining ECS integration, system lifecycle, and collision resolution pipeline.
  • Comprehensive API documentation for all public types and methods.
  • Example files demonstrating basic physics setup, rigid-body manipulation, and collision handling.

Changed #

  • Optimized SAT collision detection to avoid temporary heap allocations during overlap checks.
  • Reworked internal polygon and circle overlap helpers to use inlined double math instead of allocating intermediate vectors and lists.
  • Simplified PhysicsBody.applyForce() by removing the unused z parameter for a cleaner 2D-only API.

Notes #

  • This release is a behavior-preserving performance refactor for the 2D physics path.

1.0.0 - 2026-05-15 #

Stable release introducing native Box2D backend support with platform-aware engine selection.

Added #

  • Native Box2D v3.0 backend via Dart FFI for Android, iOS, Windows, macOS, and Linux.
  • Platform-aware backend selection through PhysicsEngineFactory.create().
  • New Box2D API surface: Box2DPhysicsEngine, Box2DWorld, Box2DBody, PhysicsGameLoop, and TransformInterpolator.
  • Native collision/contact bridge utilities including begin-contact polling and impact callback registration.
  • Bulk transform extraction from native memory to reduce per-frame overhead.
  • Native assets/tooling integration (hook/build.dart, ffigen.yaml, generated bindings, and C/C++ wrappers).
  • Graceful fallback to pure-Dart simulation when native binaries are unavailable.
  • Web-safe Box2D public stubs so the package API remains import-compatible on Flutter Web.

Changed #

  • Library exports now conditionally expose native or web Box2D APIs.
  • PhysicsEngine now includes setGravity(double gx, double gy) for runtime gravity updates across backends.
  • Collision shape math was updated to use Vector2 helpers directly and remove internal Offset extension utilities.

Notes #

  • On native platforms, PhysicsEngineFactory.create() returns the Box2D backend; on web, it returns the pure-Dart backend.
  • Box2D native sources are provided via a git submodule under src/native/third_party/box2d.

0.1.0 - 2026-05-11 #

Initial release of just_physics_engine.

Added #

  • 2D rigid-body simulation with PhysicsEngine and PhysicsBody.
  • Core physical properties including gravity, drag, restitution, friction, torque, and sleeping.
  • Collision shapes: CircleShape, RectangleShape, and PolygonShape.
  • Broad-phase collision culling via SpatialGrid.
  • Collision resolution with impulse response, friction, and positional correction.
  • Runtime simulation stats via engine.stats.
  • Debug rendering support via engine.renderDebug.
  • 2D ray utility helpers with Ray and Ray.fromPoints.
  • Initial 3D API scaffolding stubs: PhysicsEngine3D

Notes #

  • This release targets Dart SDK ^3.11.0 and Flutter >=1.17.0.
  • 2D simulation is production-ready; 3D APIs are currently stubs/scaffolding.
3
likes
160
points
561
downloads

Documentation

API reference

Publisher

verified publisherjustunknown.com

Weekly Downloads

Standalone 2D/3D physics engine for Flutter: 2D on native Box2D v3.2 (FFI) with a pure-Dart fallback, 3D on Jolt Physics through just_jolt.

Repository (GitHub)
View/report issues
Contributing

Topics

#physics #game-engine #flutter #wasm

License

BSD-3-Clause (license)

Dependencies

ffi, flutter, just_box_2d, just_dart, just_jolt, meta

More

Packages that depend on just_physics_engine