joints
FreeBodyEngine.core.physics.joints
#
Physics constraints (joints) between RigidBody2D bodies.
Every joint here is solved the same way contacts are (see world.py):
once per physics step, init_velocity_constraint() precomputes anchors
and effective mass from the bodies' current position/rotation, then
solve_velocity_constraint() runs once per solver iteration, applying a
corrective impulse that (over several iterations, interleaved with every
other joint and contact in the scene) converges the whole system toward
satisfying every constraint at once. This is what makes chained,
motor-driven limbs - like a procedurally-animated spider leg - possible:
each leg segment is its own RigidBody2D, connected to its neighbor by a
RevoluteJoint2D with a motor, and the solver reconciles all of them
together rather than one at a time.
A joint's two bodies must already be in the scene (added via
scene.add()) before the joint itself is constructed - anchors are
converted from world space to each body's local space immediately, which
needs a valid world_position/world_rotation to convert from.
MAX_JOINT_BIAS_SPEED = 4.0
module-attribute
#
DistanceJoint2D(body_a, body_b, anchor_a=None, anchor_b=None, length=None, collide_connected=False)
#
Bases: Joint2D
A rigid rod between a fixed point on each body - holds the
distance between anchor_a/anchor_b fixed at length via a stiff
Baumgarte-corrected constraint. For a springy, non-rigid version, see
SpringJoint2D.
anchor_a/anchor_b are LOCAL offsets from each body's center
(defaulting to (0, 0), the body's own center of mass) - stored
local rather than as world points, since the joint has to track
them as the bodies rotate. length defaults to the anchors'
actual distance apart at creation time.
Joint2D(body_a, body_b, collide_connected=False)
#
Bases: Node
Base class for a physics constraint between two bodies. Not a
Node2D - a joint has no position/rotation of its own, so it's added
directly under the scene root (or anywhere in the tree, really -
like everything else here it's discovered by find_nodes_with_type,
not by transform hierarchy).
collide_connected controls whether body_a/body_b still
generate contact-solver collisions with each other despite being
jointed - off by default (matching every other engine's
convention), since two directly-jointed bodies (e.g. a leg
segment's two ends) are expected to overlap/touch by
construction, and fighting that with the contact solver too would
just make the joint fight itself. body_b is None only for
joints that connect a single body to a fixed external point
rather than to another body (see TargetJoint2D).
body_a = body_a
instance-attribute
#
body_b = body_b
instance-attribute
#
collide_connected = collide_connected
instance-attribute
#
init_velocity_constraint(dt)
#
Called once per physics step, before any solver iterations - override to precompute anchors/effective mass (anything that only changes once per step, not per iteration).
solve_velocity_constraint()
#
Called once per solver iteration (several times per physics step) - override to apply this joint's corrective impulse(s).
RevoluteJoint2D(body_a, body_b, anchor, collide_connected=False, enable_motor=False, motor_speed=0.0, max_motor_torque=0.0, enable_limit=False, lower_angle=0.0, upper_angle=0.0)
#
Bases: Joint2D
A hinge: locks a shared world-space anchor point between the two
bodies (they can rotate freely around it, but not translate apart),
with an optional motor (drives their RELATIVE angular speed toward
motor_speed, clamped to max_motor_torque) and/or angle limits
(bounds their relative rotation to [lower_angle, upper_angle]
degrees, measured from whatever their relative angle happened to be
when this joint was created) - exactly what a motor-driven,
limited-range limb joint (an elbow/knee/shoulder) needs.
anchor is a WORLD-space point - both bodies must already be
in the scene so their current world transform is valid, since
it's immediately converted to a local offset on each body (so the
joint tracks the same material point on each as they move,
rather than staying fixed in world space).
anchor_a = (anchor - body_a.world_position).rotated(-body_a.world_rotation)
instance-attribute
#
anchor_b = (anchor - body_b.world_position).rotated(-body_b.world_rotation)
instance-attribute
#
beta = 0.2
instance-attribute
#
enable_limit = enable_limit
instance-attribute
#
enable_motor = enable_motor
instance-attribute
#
lower_angle = lower_angle
instance-attribute
#
max_motor_torque = max_motor_torque
instance-attribute
#
motor_speed = motor_speed
instance-attribute
#
reference_angle = body_b.world_rotation - body_a.world_rotation
instance-attribute
#
upper_angle = upper_angle
instance-attribute
#
init_velocity_constraint(dt)
#
solve_velocity_constraint()
#
SpringJoint2D(body_a, body_b, anchor_a=None, anchor_b=None, length=None, frequency=4.0, damping_ratio=0.5, collide_connected=False)
#
Bases: Joint2D
A soft distance joint - stretches/compresses springily around
length instead of holding it rigidly like DistanceJoint2D, via a
damped-spring constraint (frequency in Hz, damping_ratio from 0 =
undamped/bouncy to 1 = critically damped/no overshoot). The standard
"soft constraints" derivation (as used by Box2D's own soft distance
joint) folding the spring's frequency/damping into the impulse solve
itself, rather than applying a separate explicit spring force.
anchor_a = anchor_a if anchor_a is not None else Vector(0, 0)
instance-attribute
#
anchor_b = anchor_b if anchor_b is not None else Vector(0, 0)
instance-attribute
#
damping_ratio = damping_ratio
instance-attribute
#
frequency = frequency
instance-attribute
#
length = length
instance-attribute
#
init_velocity_constraint(dt)
#
solve_velocity_constraint()
#
TargetJoint2D(body, target, local_anchor=None, max_force=1000.0, frequency=5.0, damping_ratio=0.7)
#
Bases: Joint2D
Pulls a single point on body toward a movable world-space
target, softly (the same frequency/damping soft-constraint math as
SpringJoint2D) and clamped to max_force - for mouse-drag
interactions, a grapple hook's pull, or as the drive behind
procedural IK (aim a leg's foot at a target step position and let the
spring pull the limb chain toward it through its other joints,
instead of hand-solving the chain's inverse kinematics directly).
Only affects one body - the target point itself has no mass of its own, exactly like grabbing a body with the mouse.
damping_ratio = damping_ratio
instance-attribute
#
frequency = frequency
instance-attribute
#
local_anchor = local_anchor if local_anchor is not None else Vector(0, 0)
instance-attribute
#
max_force = max_force
instance-attribute
#
target = target.copy()
instance-attribute
#
init_velocity_constraint(dt)
#
solve_velocity_constraint()
#
WeldJoint2D(body_a, body_b, anchor, collide_connected=False)
#
Bases: Joint2D
Rigidly fuses two bodies together at a shared world-space anchor
point AND their current relative angle - like RevoluteJoint2D with
its rotational freedom locked too, so the pair behaves as a single
rigid body while still being two separate (and later detachable)
ones. Useful for permanently gluing parts together without folding
them into a single RigidBody2D/collider.