Learn Simulant
Everything you need to know to build games with Simulant
Colliders
Colliders (also called fixtures) define the physical shape and surface properties of a physics body. They determine how bodies interact during collisions. This guide covers collider shapes, materials, collision filtering, and sensors.
Related documentation: Physics Overview, Rigid Bodies, Joints.
1. What is a Collider?
A collider is a geometric shape attached to a PhysicsBody. The physics engine uses these shapes to detect and resolve collisions. A single body can have multiple colliders attached to it.
Each collider carries:
- A shape (box, sphere, capsule, triangle, or mesh)
- A PhysicsMaterial (density, friction, bounciness)
- A kind value (
uint16_t) used for collision filtering - A position and rotation relative to the body
2. Collider Shapes
2.1 Box Collider
The most versatile and performant collider. Boxes are defined by their half-extents (size along each axis from the center).
PhysicsMaterial mat = PhysicsMaterial::wood();
// Simple box centered on the body
body->add_box_collider(Vec3(1, 0.5f, 1), mat);
// Box with offset (e.g., a collider above the body center)
body->add_box_collider(
Vec3(1, 0.5f, 1), // Half-extents
mat, // Material
0, // Kind (collision layer)
Vec3(0, 1, 0), // Offset from body center
Quaternion() // Rotation (none)
);
// Rotated box collider
body->add_box_collider(
Vec3(1, 0.5f, 1),
mat,
0,
Vec3(),
Quaternion::from_axis_angle(Vec3(0, 1, 0), 45)
);
Use for: Crates, walls, platforms, most rectangular objects, character hitboxes.
2.2 Sphere Collider
Spheres are defined by their diameter. They are the cheapest collider to compute and are ideal for round objects.
PhysicsMaterial mat = PhysicsMaterial::rubber();
// Sphere centered on the body
body->add_sphere_collider(1.0f, mat); // 1.0 = diameter
// Offset sphere (e.g., a ball at the end of a chain)
body->add_sphere_collider(
0.5f, // Diameter
mat, // Material
0, // Kind
Vec3(0, -2, 0) // Offset
);
Use for: Balls, wheels, simple character bounds, pickup triggers.
2.3 Capsule Collider
Capsules are defined by two endpoints and a diameter. They are cylinders with hemispherical caps, ideal for character controllers.
PhysicsMaterial mat = PhysicsMaterial::wood();
// Vertical capsule (typical for a standing character)
body->add_capsule_collider(
Vec3(0, -0.5f, 0), // Bottom endpoint
Vec3(0, 0.5f, 0), // Top endpoint
0.3f, // Diameter
mat // Material
);
// Horizontal capsule
body->add_capsule_collider(
Vec3(-0.5f, 0, 0), // Left endpoint
Vec3(0.5f, 0, 0), // Right endpoint
0.2f, // Diameter
mat
);
Use for: Characters, pills, elongated round objects.
2.4 Triangle Collider
A single triangle defined by three vertices. Useful for flat surfaces or as building blocks for more complex shapes.
PhysicsMaterial mat = PhysicsMaterial::stone();
body->add_triangle_collider(
Vec3(-1, 0, -1), // Vertex 1
Vec3(1, 0, -1), // Vertex 2
Vec3(0, 0, 1), // Vertex 3
mat, // Material
0 // Kind
);
Use for: Ramps, inclined planes, simple terrain features.
2.5 Mesh Collider
Mesh colliders use the triangles from a MeshPtr to create complex collision geometry. Only available on StaticBody.
auto mesh = assets->load_mesh("models/building.obj");
auto body = create_child<StaticBody>();
PhysicsMaterial mat = PhysicsMaterial::stone();
body->add_mesh_collider(mesh, mat);
You can also position, orient, and scale the mesh collider independently:
body->add_mesh_collider(
mesh,
mat,
0, // Kind (collision layer)
Vec3(0, 5, 0), // Position offset
Quaternion(), // Orientation
Vec3(2, 1, 2) // Scale
);
Use for: Complex static geometry like buildings, terrain, sculptures.
Warning: Mesh colliders are significantly more expensive than primitive shapes. Use them sparingly and only for static geometry.
3. Physics Materials
Materials define the surface properties of a collider. They control how the collider interacts physically with other colliders.
3.1 Built-in Materials
PhysicsMaterial wood = PhysicsMaterial::wood();
PhysicsMaterial rubber = PhysicsMaterial::rubber();
PhysicsMaterial iron = PhysicsMaterial::iron();
PhysicsMaterial stone = PhysicsMaterial::stone();
| Material | Density | Friction | Bounciness | Typical Use |
|---|---|---|---|---|
| Wood | 0.005 | 0.4 | 0.2 | Crates, wooden platforms |
| Rubber | 0.001 | 0.3 | 0.8 | Balls, bumpers |
| Iron | 0.1 | 0.2 | ~0 | Heavy machinery, metal objects |
| Stone | 0.1 | 0.8 | ~0 | Ground, walls, terrain |
3.2 Custom Materials
// density, friction, bounciness
PhysicsMaterial ice(0.001f, 0.0f, 0.1f); // Slippery
PhysicsMaterial superball(0.001f, 0.1f, 0.95f); // Very bouncy
PhysicsMaterial sandpaper(0.005f, 1.0f, 0.0f); // Very grippy
3.3 Density Multipliers
Built-in materials accept an optional density multiplier:
PhysicsMaterial light_wood = PhysicsMaterial::wood(0.5f); // Half density
PhysicsMaterial heavy_iron = PhysicsMaterial::iron(5.0f); // 5x density
3.4 Material Property Reference
| Property | Description | Range | Effect |
|---|---|---|---|
density |
Mass per unit volume | 0.0+ | Higher = heavier body |
friction |
Surface grip | 0.0--1.0 | 0 = ice, 1 = sandpaper |
bounciness |
Restitution coefficient | 0.0--1.0 | 0 = dead, 1 = perfectly elastic |
4. Collision Filtering
The kind parameter on every collider is a uint16_t value used with a ContactFilter to control which colliders can collide with each other.
4.1 Creating a Contact Filter
class GameContactFilter : public ContactFilter {
public:
bool should_collide(const Fixture* lhs, const Fixture* rhs) const override {
uint16_t kind_a = lhs->kind();
uint16_t kind_b = rhs->kind();
// Define collision categories using bit flags
constexpr uint16_t PLAYER = 1 << 0;
constexpr uint16_t ENEMY = 1 << 1;
constexpr uint16_t BULLET = 1 << 2;
constexpr uint16_t PICKUP = 1 << 3;
// Players don't collide with other players
if (kind_a == PLAYER && kind_b == PLAYER) return false;
// Enemies don't collide with other enemies
if (kind_a == ENEMY && kind_b == ENEMY) return false;
// Bullets hit everything except pickups
if (kind_a == BULLET && kind_b == PICKUP) return false;
if (kind_b == BULLET && kind_a == PICKUP) return false;
return true;
}
bool should_respond(const Fixture* lhs, const Fixture* rhs) const override {
// Return false to detect collision but not physically respond
// (useful for trigger zones that still report contact events)
return true;
}
};
4.2 Applying the Filter
auto filter = std::make_unique<GameContactFilter>();
physics_service->set_contact_filter(filter.get());
// Keep the filter alive for as long as the simulation runs
// (the service holds a weak reference)
4.3 Using Kind Values
When creating colliders, assign kind values that match your filter logic:
constexpr uint16_t PLAYER = 1 << 0;
constexpr uint16_t ENEMY = 1 << 1;
// Player collider
player_body->add_capsule_collider(
Vec3(0, -0.5f, 0), Vec3(0, 0.5f, 0), 0.3f,
PhysicsMaterial::wood(), PLAYER
);
// Enemy collider
enemy_body->add_box_collider(
Vec3(0.5f, 1, 0.5f),
PhysicsMaterial::wood(), ENEMY
);
5. Sensors
Sensors are colliders that detect overlaps without causing a physical response. Objects pass through sensors but collision events are still fired.
5.1 Creating Sensors
The physics system uses ContactFilter::should_respond() to determine whether a collision should be physical or sensor-only. Return false from should_respond() for sensor behavior:
class SensorContactFilter : public ContactFilter {
public:
bool should_collide(const Fixture* lhs, const Fixture* rhs) const override {
// Sensors use a specific kind value
constexpr uint16_t SENSOR = 1 << 15;
if (lhs->kind() == SENSOR || rhs->kind() == SENSOR) {
return true; // Detect the overlap
}
return true;
}
bool should_respond(const Fixture* lhs, const Fixture* rhs) const override {
constexpr uint16_t SENSOR = 1 << 15;
// If either fixture is a sensor, don't physically respond
if (lhs->kind() == SENSOR || rhs->kind() == SENSOR) {
return false;
}
return true;
}
};
5.2 Sensor Use Cases
constexpr uint16_t SENSOR = 1 << 15;
// Pickup trigger zone
auto pickup = create_child<Actor>();
auto body = pickup->create_child<StaticBody>();
body->add_sphere_collider(1.0f, PhysicsMaterial(), SENSOR);
body->signal_collision_enter().connect([](const Collision& collision) {
S_INFO("Player entered pickup zone!");
// Trigger pickup logic
});
// Door trigger zone
auto door_trigger = create_child<Actor>();
auto trigger_body = door_trigger->create_child<StaticBody>();
trigger_body->add_box_collider(Vec3(1, 2, 0.5f), PhysicsMaterial(), SENSOR);
trigger_body->signal_collision_enter().connect([](const Collision& collision) {
open_door();
});
trigger_body->signal_collision_exit().connect([](const Collision& collision) {
close_door();
});
6. Multiple Fixtures per Body
A single physics body can have multiple colliders. This is useful for complex shapes or for combining physical colliders with sensors.
6.1 Compound Colliders
auto body = actor->create_child<DynamicBody>();
PhysicsMaterial mat = PhysicsMaterial::wood();
// L-shaped object made of two boxes
body->add_box_collider(Vec3(1, 0.2f, 0.2f), mat, 0, Vec3(0, 0, 0));
body->add_box_collider(Vec3(0.2f, 1, 0.2f), mat, 0, Vec3(-0.8f, 0.8f, 0));
6.2 Physical Collider + Sensor
auto body = actor->create_child<DynamicBody>();
// Main physical collider
body->add_box_collider(Vec3(0.5f, 1, 0.5f), PhysicsMaterial::wood());
// Ground detection sensor at the feet
constexpr uint16_t SENSOR = 1 << 15;
body->add_box_collider(
Vec3(0.4f, 0.05f, 0.4f),
PhysicsMaterial(), // Material doesn't matter for sensors
SENSOR,
Vec3(0, -1.05f, 0) // Just below the feet
);
Check ground contact via the contact list:
bool is_grounded() {
for (auto it = body->contacts().begin(); it != body->contacts().end(); ++it) {
Contact contact = *it;
// Check if the contact involves the sensor fixture
// (you'd check fixture kind here)
}
return false;
}
7. The Fixture Class
Internally, colliders are represented by the Fixture class. You generally don't create fixtures directly -- they are created through the body's add_*_collider methods. The Fixture class provides:
class Fixture {
public:
const PhysicsBody* body() const; // Get the body this fixture belongs to
uint16_t kind() const; // Get the kind value
};
Fixtures are referenced in collision callbacks and through the ContactList:
struct Contact {
Contact(const Fixture& a, const Fixture& b) : fixtures{a, b} {}
Fixture fixtures[2];
};
8. Collider Performance
Collider choice has a significant impact on physics performance. Here is the approximate cost from cheapest to most expensive:
| Shape | Relative Cost | Notes |
|---|---|---|
| Sphere | Lowest | Single radius check, very fast |
| Capsule | Low | Two sphere + cylinder tests |
| Box | Low-Medium | SAT (Separating Axis Theorem) |
| Triangle | Medium | Plane and edge tests |
| Mesh | Highest | Triangle soup, many tests |
Performance Tips
- Prefer primitive shapes over mesh colliders whenever possible.
- Use the simplest shape that accurately represents your object.
- Combine multiple simple colliders rather than using a single mesh collider.
- Avoid mesh colliders on dynamic bodies -- they are only available on
StaticBodyfor a reason. - Use sphere colliders for triggers when the exact shape doesn't matter.
9. Common Patterns
9.1 Character Controller Setup
class CharacterBehaviour : public StageNode {
public:
FindResult<DynamicBody> body = smlt::FindDescendent<DynamicBody>(this);
void on_load() override {
PhysicsMaterial mat = PhysicsMaterial::wood();
// Body collider (capsule)
body->add_capsule_collider(
Vec3(0, -0.4f, 0),
Vec3(0, 0.6f, 0),
0.3f,
mat
);
// Ground sensor
constexpr uint16_t SENSOR = 1 << 15;
body->add_sphere_collider(0.1f, PhysicsMaterial(), SENSOR, Vec3(0, -0.55f, 0));
}
bool is_grounded() const {
// Check contacts for ground sensor
for (auto it = body->contacts().begin(); it != body->contacts().end(); ++it) {
// Check if contact normal points upward
// (simplified -- you'd check the specific fixture)
}
return false;
}
};
9.2 Vehicle with Wheel Sensors
auto chassis = create_child<DynamicBody>();
chassis->add_box_collider(Vec3(1, 0.3f, 0.5f), PhysicsMaterial::iron());
// Wheel contact sensors (for suspension/ground detection)
constexpr uint16_t WHEEL_SENSOR = 1 << 5;
chassis->add_sphere_collider(0.1f, PhysicsMaterial(), WHEEL_SENSOR, Vec3(-0.8f, -0.5f, 0.4f));
chassis->add_sphere_collider(0.1f, PhysicsMaterial(), WHEEL_SENSOR, Vec3(-0.8f, -0.5f, -0.4f));
chassis->add_sphere_collider(0.1f, PhysicsMaterial(), WHEEL_SENSOR, Vec3(0.8f, -0.5f, 0.4f));
chassis->add_sphere_collider(0.1f, PhysicsMaterial(), WHEEL_SENSOR, Vec3(0.8f, -0.5f, -0.4f));
9.3 Room with Mesh Walls
auto room_mesh = assets->load_mesh("models/room.obj");
auto room = create_child<StaticBody>();
room->add_mesh_collider(room_mesh, PhysicsMaterial::stone());
See Also
- Physics Overview -- General physics introduction
- Rigid Bodies -- Body types and properties
- Joints -- Connecting bodies together
- Raycasting -- Querying the physics world
- Best Practices -- Optimization and patterns