just_physics_engine 1.3.0
just_physics_engine: ^1.3.0 copied to clipboard
Standalone 2D/3D physics engine for Flutter — pure-Dart fallback + native Box2D v3.0 FFI backend.
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 writingPhysicsBody.positionworked on web but was silently discarded on native (there was nob2w_setTransform), so respawns, checkpoints, level-load placement and warps did nothing on desktop and mobile.BodyType { static, kinematic, dynamic }andPhysicsBody.bodyType. Kinematic bodies are moved only by explicit velocity writes and cannot be pushed — moving platforms, elevators and crushers.mass <= 0still forces static viaeffectiveBodyType, so nothing that predates this shifts.PhysicsBody.gravityScale— a real per-body float, where only the booleanuseGravityexisted before. ReadeffectiveGravityScale, which folds both together. This is what variable jump height is built from.- One-way platforms on the native backend.
isOneWaywas honoured only by the pure-Dart resolver. There is now a Box2D pre-solve contact filter with matching semantics, plusPhysicsBody.oneWayDirectionto pick which side is solid. - Native ray casting and AABB queries.
castRay,castRayAllandqueryAABBnow 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, asdragis 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
alpharemainder, 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 withPhysicsEngine.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 stalebox2d_flutter.dllused to report a healthy'box2d_v3'backend and then throw "Failed to lookup symbol" mid-game, on the first respawn. It now fails duringinitialize()and falls back cleanly to pure Dart with an honest'dart_fallback'.b2w_createDynamicBody/b2w_createStaticBodynow forward tob2w_createBody; their signatures are unchanged.- The pure-Dart backend's
dragno longer slows spin;angularDampingdoes, as on Box2D. A body that relied on drag to stop spinning on the web setsangularDampinginstead. - 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_addCircleShapetakes the centre after the body handle;b2w_bulkExtractTransformswrites 7 floats per body (the 7th the angular velocity).
Fixed #
- Contact events never fired on the native backend.
b2DefaultShapeDef()zero-initialises, soenableContactEventsdefaulted to false and the wrapper never set it. Collisions resolved correctly — bodies landed and stacked as expected — butpollContactBeginEventsandpollContactEndEventsstayed permanently silent, while the pure-Dart backend generated events normally. Anything built on contacts, includingPhysicsBodyComponent.isGroundedand 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. applyLinearImpulseon the native backend mirrors the resulting velocity back into the Dart body.update()pushesbody.velocityinto 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.setBodyTransformwrites 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.dragwas pure-Dart only, so air drag existed on web and not on device. - Spin on the native backend.
PhysicsBody.angularVelocitywas 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
addBodywrote the flag only for sensors. A solid body given a dead sensor's handle let everything through.addBodynow 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 oncedrag * 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 #
setBodySensorremains 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 withisActiveor 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: truefor 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 olddart.library.htmlconditional import that 1.2.1 removed everywhere else. On WASM web builds, wheredart.library.htmlis not defined, this resolved to the native Box2D FFI backend instead of the pure-Dart fallback, pullingdart:ffiinto the web/wasm compile graph and breaking the build. All conditional imports now consistently key offdart.library.io.- Native contact-begin events (
b2w_getContactBeginEvent) always reported a(0, 0)contact normal instead of the real collision normal. Now reads it fromb2Contact_GetData. Box2DJoint.reactionForcealways returnedOffset.zeroeven 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 calleddestroy()on native joints before tearing down the world, leaving any retainedBox2DJointin a stale, not-_destroyedstate.RevoluteJoint(pure-Dart) acceptedlimitEnabled/lowerLimit/upperLimitbut never enforced them — angle limits are now applied duringapplyConstraint.ChainShapewas not recognized as a valid collision partner byCircleShape,PolygonShape,CapsuleShape, andRoundedPolygonShape— collisions against chain terrain were silently missed depending on which body was added to the engine first. All four now delegate correctly.- A plain
PolygonShapecolliding against aRoundedPolygonShapeignored the other shape'scornerRadiuswhen 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_createWorldnever calledb2SetLengthUnitsPerMeter, 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 = falsehad no effect on the native backend — nothing set the native body'sgravityScale. Addedb2w_setBodyGravityScale, called fromaddBody().PhysicsBody.isAwakewas never synchronized with native Box2D, in either direction: a body created withisAwake: falseactually 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. Addedb2w_setBodyAwake(pushed once at creation) and extendedb2w_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-onlyacceleration/torquefields, which nothing read on this backend.Box2DPhysicsEngine.update()now pushes them throughb2w_applyForce/b2w_applyTorqueeach 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 fixeddensity = 1.0, so e.g. aCircleShape(5.0)massed ~78.5 regardless of whatPhysicsBody.masssaid — silently distorting every force, impulse, and collision response. Addedb2w_setBodyMass, which overrides Box2D's shape-derived mass withPhysicsBody.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
Box2DWorldspun up its ownThreadPoolsized tohardware_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 explicitnumThreads. - 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/.cppdeclared allb2w_*functions with no export annotation; unlike ELF/Mach-O, Windows DLLs export nothing by default, solink.exesilently produced a DLL with an empty export table. Everydart:ffiDynamicLibrary.lookup()call failed with "procedure not found" even though the.dllitself loaded fine. Added aB2W_EXPORTmacro (__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 withmin(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 tomax(1.0, ball) == 1.0on the native backend, making every ball bounce identically regardless of its own restitution. Installed a matching min-mixingrestitutionCallbackon 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, soSetMotorSpeed/EnableMotorsilently produced no motion until something else disturbed it. Revolute, prismatic, and wheel joint motors now explicitly wake both jointed bodies when enabled. PhysicsBody.velocitywrites 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 fromVelocityComponent) 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 currentvelocityinto the native body before stepping. Likewise, a body's initialvelocityset at construction time is now carried over to the native body when added — previously it silently started at rest.
Added #
- Pure-Dart
WheelJointand unifiedaddWheelJoint()onPhysicsEngine/Box2DPhysicsEngine. PreviouslycreateWheelJointonly 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-DartPhysicsEngine. 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.isOneWayon the pure-Dart engine — contacts against anisOneWaybody 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 upstreamerincatto/box2d, atsrc/native/third_party/just_box_2d.
1.2.1 - 2026-06-11 #
Metadata and compatibility patch release focused on clearer pub.flutter-io.cn platform signaling and safer WebAssembly target behavior.
Added #
- Explicit pub.flutter-io.cn 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.iotargets 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, andRoundedPolygonShape. - Compound-body support through
PhysicsBody.additionalShapes,PhysicsBody.isCompound, and aggregate bounds handling for broad-phase collision. - World query APIs:
castRay(),castRayAll(),castCircle(),queryAABB(),queryCircle(), andqueryPoint(). - New query result types:
RayBodyHitandShapeCastResult. - Joint constraint support for the pure-Dart engine:
DistanceJoint,MouseJoint,WeldJoint,RevoluteJoint,PrismaticJoint, andWheelJointvia the unified engine API. - Native Box2D joint wrappers and configuration helpers through
Box2DJointplus 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, andisBulletonPhysicsBody. - 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.
PhysicsEngine3Dis 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 unusedzparameter 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, andTransformInterpolator. - 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.
PhysicsEnginenow includessetGravity(double gx, double gy)for runtime gravity updates across backends.- Collision shape math was updated to use
Vector2helpers directly and remove internalOffsetextension 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.