Learn Simulant
Everything you need to know to build games with Simulant
Joints
Joints connect two physics bodies together, constraining their relative motion. Simulant provides the SphereJoint for creating ball-and-socket style connections. This guide covers how to create, configure, and manage joints.
Related documentation: Physics Overview, Rigid Bodies, Colliders.
1. What is a Joint?
A joint is a constraint between two ReactiveBody instances. It restricts how the bodies can move relative to each other. Joints are commonly used for:
- Ragdoll characters
- Chains and ropes
- Door hinges
- Vehicle suspensions
- Pendulums
Joints are managed by the PhysicsService and are automatically cleaned up when either connected body is destroyed.
2. SphereJoint
The SphereJoint (also known as a ball-and-socket joint) connects two bodies at anchor points, allowing the second body to rotate freely around the anchor within a specified radius.
2.1 Creating a SphereJoint
// Create two bodies
auto anchor_body = anchor_actor->create_child<DynamicBody>();
anchor_body->add_sphere_collider(0.3f, PhysicsMaterial::iron());
anchor_body->set_mass(5.0f);
auto swinging_body = pendulum_actor->create_child<DynamicBody>();
swinging_body->add_box_collider(Vec3(0.2f, 1, 0.2f), PhysicsMaterial::wood());
swinging_body->set_mass(1.0f);
// Create the joint
auto joint = anchor_body->create_sphere_joint(
swinging_body, // The other body to connect
Vec3(0, 0, 0), // Anchor point relative to anchor_body
Vec3(0, 0.5f, 0) // Anchor point relative to swinging_body
);
The joint is created on the ReactiveBody (which both DynamicBody and KinematicBody inherit from). The method signature is:
SphereJoint* create_sphere_joint(
ReactiveBody* other,
const Vec3& this_relative_anchor,
const Vec3& other_relative_anchor
);
2.2 Anchor Points
Anchor points are specified in local body space. This means the anchors move and rotate with their respective bodies.
// Anchor at the top of the swinging body (local space)
Vec3 swing_anchor = Vec3(0, 0.5f, 0);
// Anchor at the bottom of the anchor body (local space)
Vec3 body_anchor = Vec3(0, -0.3f, 0);
auto joint = anchor_body->create_sphere_joint(
swinging_body,
body_anchor, // Where on the anchor body
swing_anchor // Where on the swinging body
);
2.3 Joint Ownership
When you create a joint, it is owned by both connected bodies. The joint is stored as a std::shared_ptr<SphereJoint> in each body's internal joint list:
class ReactiveBody {
private:
std::vector<std::shared_ptr<SphereJoint>> sphere_joints_;
};
Each body tracks its joints:
std::size_t count = body->sphere_joint_count();
3. Joint Lifecycle
3.1 Automatic Cleanup
When a body with joints is destroyed, all its joints are automatically cleaned up:
// ReactiveBody::on_destroy() handles this:
// 1. Remove each joint from the other body's joint list
// 2. Clear (destroy) all joints, which unregisters them from PhysicsService
body->destroy(); // Joints are automatically cleaned up
3.2 Manual Joint Removal
Joints are reference-counted through shared_ptr. When all references to a joint are removed, it is destroyed and unregistered from the physics simulation:
// If you have access to the joint pointer:
// (Note: the API currently doesn't expose the joint list publicly)
// The joint will be cleaned up when either body is destroyed
For most use cases, you don't need to manually manage joints. Simply destroying either connected body will clean up the joint.
4. Common Joint Patterns
4.1 Pendulum
class PendulumBehaviour : public StageNode {
public:
FindResult<DynamicBody> anchor = smlt::FindDescendent<DynamicBody>("Anchor", this);
FindResult<DynamicBody> bob = smlt::FindDescendent<DynamicBody>("Bob", this);
void on_load() override {
// Set up anchor body (static-feeling, heavy)
anchor->add_sphere_collider(0.2f, PhysicsMaterial::iron());
anchor->set_mass(100.0f); // Very heavy = barely moves
anchor->lock_rotation(true, true, true); // Don't rotate
anchor->set_linear_damping(1.0f); // Dampen any movement
// Set up pendulum bob
bob->add_box_collider(Vec3(0.1f, 0.3f, 0.1f), PhysicsMaterial::iron());
bob->set_mass(1.0f);
// Connect them
anchor->create_sphere_joint(
bob,
Vec3(0, 0, 0), // Center of anchor
Vec3(0, 0.3f, 0) // Top of bob
);
}
};
4.2 Ragdoll Chain
class RagdollBehaviour : public StageNode {
public:
void on_load() override {
PhysicsMaterial mat = PhysicsMaterial::wood();
// Create body segments
auto head = create_body_segment(Vec3(0.3f, 0.3f, 0.3f), mat, Vec3(0, 2.5f, 0));
auto torso = create_body_segment(Vec3(0.4f, 0.6f, 0.3f), mat, Vec3(0, 1.7f, 0));
auto hips = create_body_segment(Vec3(0.35f, 0.3f, 0.25f), mat, Vec3(0, 1.1f, 0));
// Connect head to torso
torso->create_sphere_joint(
head,
Vec3(0, 0.6f, 0), // Top of torso
Vec3(0, -0.3f, 0) // Bottom of head
);
// Connect torso to hips
hips->create_sphere_joint(
torso,
Vec3(0, 0.3f, 0), // Top of hips
Vec3(0, -0.6f, 0) // Bottom of torso
);
}
DynamicBody* create_body_segment(Vec3 size, PhysicsMaterial mat, Vec3 pos) {
auto actor = create_child<Actor>();
auto mesh = assets->new_mesh_from_procedural_cube();
actor->set_mesh(mesh->id());
actor->scale_by(size.x * 2, size.y * 2, size.z * 2);
actor->move_to(pos);
auto body = actor->create_child<DynamicBody>();
body->add_box_collider(size, mat);
body->set_mass(1.0f);
return body;
}
};
4.3 Rope / Chain
class RopeBehaviour : public StageNode {
public:
FindResult<DynamicBody> anchor = smlt::FindDescendent<DynamicBody>("Anchor", this);
void on_load() override {
// Anchor point (fixed)
anchor->add_sphere_collider(0.1f, PhysicsMaterial::iron());
anchor->set_mass(1000.0f);
anchor->lock_rotation(true, true, true);
anchor->set_linear_damping(1.0f);
// Create chain links
int num_links = 10;
float link_size = 0.2f;
DynamicBody* previous = anchor;
for (int i = 0; i < num_links; ++i) {
auto link_actor = create_child<Actor>();
auto mesh = assets->new_mesh_from_procedural_cube();
link_actor->set_mesh(mesh->id());
link_actor->scale_by(link_size, link_size, link_size);
link_actor->move_to(0, 2.0f - i * link_size * 2, 0);
auto link_body = link_actor->create_child<DynamicBody>();
link_body->add_box_collider(
Vec3(link_size / 2, link_size / 2, link_size / 2),
PhysicsMaterial::wood()
);
link_body->set_mass(0.5f);
// Connect to previous link
previous->create_sphere_joint(
link_body,
Vec3(0, -link_size / 2, 0), // Bottom of previous
Vec3(0, link_size / 2, 0) // Top of current
);
previous = link_body;
}
}
};
4.4 Door Hinge
class DoorBehaviour : public StageNode {
public:
FindResult<KinematicBody> door_frame = smlt::FindDescendent<KinematicBody>("Frame", this);
FindResult<DynamicBody> door_panel = smlt::FindDescendent<DynamicBody>("Door", this);
void on_load() override {
// Door frame (kinematic = doesn't move from physics)
door_frame->add_box_collider(Vec3(0.1f, 2, 0.1f), PhysicsMaterial::stone());
// Door panel
door_panel->add_box_collider(Vec3(0.05f, 1, 0.4f), PhysicsMaterial::wood());
door_panel->set_mass(2.0f);
// Hinge joint at the door edge
door_frame->create_sphere_joint(
door_panel,
Vec3(0, 0, 0), // Frame hinge point
Vec3(0, 0, 0.4f) // Door edge (hinge side)
);
}
};
5. PhysicsService and Joint Management
Joints are created and managed by the PhysicsService. When you call create_sphere_joint() on a body, the service initializes the underlying physics joint:
// From joints.cpp:
SphereJoint::SphereJoint(ReactiveBody* a, ReactiveBody* b,
const Vec3& aoff, const Vec3& boff)
: impl_(new SphereJointImpl()) {
auto sim = a->scene->find_service<PhysicsService>();
if (sim) {
sim->init_sphere_joint(this, a, b, aoff, boff);
}
impl_->body_a_ = a;
impl_->body_b_ = b;
}
When the joint is destroyed, it is released from the service:
SphereJoint::~SphereJoint() {
auto sim = impl_->body_a_->scene->find_service<PhysicsService>();
if (sim) {
sim->release_sphere_joint(this);
}
}
Internal Implementation
The joint system uses the Bounce physics library internally. The SphereJoint holds a pointer to the underlying b3Joint:
struct SphereJointImpl {
b3Joint* joint_ = nullptr;
ReactiveBody* body_a_ = nullptr;
ReactiveBody* body_b_ = nullptr;
};
6. Joint Limitations and Notes
6.1 Current Joint Type
Simulant currently provides only SphereJoint. This is a ball-and-socket joint that constrains the connected bodies to pivot around anchor points. Other joint types (hinge, slider, fixed, etc.) are not currently exposed in the public API.
6.2 Body Requirements
Both bodies connected to a joint must be ReactiveBody instances (i.e., DynamicBody or KinematicBody). StaticBody cannot participate in joints because it has no physics simulation state.
If you need a joint anchored to a fixed point, create a very heavy DynamicBody with locked rotation and high damping to act as an immovable anchor:
auto anchor = create_child<DynamicBody>();
anchor->add_sphere_collider(0.1f, PhysicsMaterial::iron());
anchor->set_mass(1000.0f);
anchor->lock_rotation(true, true, true);
anchor->set_linear_damping(1.0f);
6.3 Joint Count Tracking
Each body tracks how many joints it participates in:
std::size_t count = body->sphere_joint_count();
This is useful for debugging and for ensuring joints are properly cleaned up.
7. Debugging Joints
7.1 Visualizing Joints
Enable physics debug visualization to see joints rendered in the scene:
auto debug = /* your Debug node */;
physics_service->set_debug(debug);
7.2 Checking Joint State
// Check how many joints a body has
S_INFO("Body has {} joints", body->sphere_joint_count());
// Access joint body references
ReactiveBody* body_a = joint->first_body();
ReactiveBody* body_b = joint->second_body();
7.3 Common Issues
| Problem | Cause | Solution |
|---|---|---|
| Joint doesn't form | One body has no PhysicsService | Ensure both bodies are in a scene with a PhysicsService |
| Bodies separate | Anchor points too far apart | Ensure anchor points are at the same world position when created |
| Joint doesn't clean up | Body destroyed without cleanup | Bodies auto-cleanup joints in on_destroy() |
| Bodies jitter | Mass ratio too extreme | Keep mass ratios under 10:1 for stable joints |
See Also
- Physics Overview -- General physics introduction
- Rigid Bodies -- Body types and properties
- Colliders -- Collider shapes and materials
- Raycasting -- Querying the physics world
- Best Practices -- Optimization and patterns