Physics API¶
Citrus Engine provides a flexible physics system with support for multiple backends. This document covers how to use physics in your game.
Overview¶
The physics system is backend-agnostic, allowing you to choose between:
- Jolt Physics - High-performance, modern physics engine
- Bullet3 - Widely-used open-source physics
- PhysX (future) - Industry-standard physics from NVIDIA
All backends use the same ECS component interface, so switching backends doesn't require code changes.
Choosing a Physics Backend¶
Import a Physics Module¶
Select one physics backend by importing its module:
import engine;
// Initialize engine
engine::Engine eng;
eng.Init(1280, 720);
// Import Jolt Physics backend
eng.ecs.GetWorld().import<engine::physics::JoltPhysicsModule>();
// OR import Bullet3 backend
// eng.ecs.GetWorld().import<engine::physics::Bullet3PhysicsModule>();
Note
Only import one physics module per world. Importing multiple backends will cause conflicts.
Physics Components¶
RigidBody¶
The RigidBody component defines the physical properties of an entity:
struct RigidBody {
MotionType motion_type{MotionType::Dynamic};
float mass{1.0f};
float linear_damping{0.05f};
float angular_damping{0.05f};
float friction{0.5f};
float restitution{0.3f};
bool enable_ccd{false}; // Continuous collision detection
bool use_gravity{true};
float gravity_scale{1.0f};
CollisionLayer layer{CollisionLayer::Default};
uint32_t collision_mask{0xFFFFFFFF};
};
Motion Types:
MotionType::Static- Doesn't move (terrain, walls)MotionType::Dynamic- Fully simulated (players, projectiles)MotionType::Kinematic- Moved by user, affects others (moving platforms)
Example:
auto entity = eng.ecs.CreateEntity("Box");
entity.set<engine::components::Transform>({{0.0f, 5.0f, 0.0f}});
entity.set<engine::physics::RigidBody>({
.motion_type = engine::physics::MotionType::Dynamic,
.mass = 10.0f,
.friction = 0.6f,
.restitution = 0.4f // Bounciness
});
CollisionShape¶
The CollisionShape component defines the collision geometry:
struct CollisionShape {
ShapeType type{ShapeType::Box};
// Box parameters
glm::vec3 box_half_extents{0.5f, 0.5f, 0.5f};
// Sphere parameters
float sphere_radius{0.5f};
// Capsule parameters
float capsule_radius{0.5f};
float capsule_height{1.0f};
// Cylinder parameters
float cylinder_radius{0.5f};
float cylinder_height{1.0f};
// Offset from entity transform
glm::vec3 offset{0.0f};
glm::quat rotation{1.0f, 0.0f, 0.0f, 0.0f};
};
Shape Types:
ShapeType::Box- Rectangular boxShapeType::Sphere- SphereShapeType::Capsule- Capsule (pill shape)ShapeType::Cylinder- Cylinder
Example:
// Box shape
entity.set<engine::physics::CollisionShape>({
.type = engine::physics::ShapeType::Box,
.box_half_extents = {1.0f, 1.0f, 1.0f} // 2x2x2 box
});
// Sphere shape
entity.set<engine::physics::CollisionShape>({
.type = engine::physics::ShapeType::Sphere,
.sphere_radius = 0.5f
});
// Capsule for characters
entity.set<engine::physics::CollisionShape>({
.type = engine::physics::ShapeType::Capsule,
.capsule_radius = 0.5f,
.capsule_height = 2.0f,
.offset = {0.0f, 1.0f, 0.0f} // Offset up
});
PhysicsVelocity¶
Stores the current velocity of a physics body:
struct PhysicsVelocity {
glm::vec3 linear{0.0f}; // Linear velocity (m/s)
glm::vec3 angular{0.0f}; // Angular velocity (rad/s)
};
Example:
// Set initial velocity
entity.set<engine::physics::PhysicsVelocity>({
.linear = {5.0f, 0.0f, 0.0f} // Move right at 5 m/s
});
// Read velocity
auto velocity = entity.get<engine::physics::PhysicsVelocity>();
std::cout << "Speed: " << glm::length(velocity->linear) << " m/s" << std::endl;
PhysicsForce¶
Apply forces and torques to bodies:
struct PhysicsForce {
glm::vec3 force{0.0f};
glm::vec3 torque{0.0f};
bool clear_after_apply{true}; // Reset after physics step
};
Example:
// Apply upward force (jump)
entity.set<engine::physics::PhysicsForce>({
.force = {0.0f, 500.0f, 0.0f},
.clear_after_apply = true
});
// Continuous force (thruster)
entity.set<engine::physics::PhysicsForce>({
.force = {0.0f, 10.0f, 0.0f},
.clear_after_apply = false // Keep applying
});
PhysicsImpulse¶
Apply instantaneous impulses (consumed immediately):
struct PhysicsImpulse {
glm::vec3 impulse{0.0f};
glm::vec3 point{0.0f}; // World-space application point
};
Example:
// Apply impulse at center of mass
entity.set<engine::physics::PhysicsImpulse>({
.impulse = {100.0f, 0.0f, 0.0f}
});
// Apply impulse at specific point (creates torque)
entity.set<engine::physics::PhysicsImpulse>({
.impulse = {0.0f, 50.0f, 0.0f},
.point = {1.0f, 0.0f, 0.0f} // Apply to right side
});
Tag Components¶
IsTrigger¶
Mark a body as a trigger volume (no physical collision response):
Triggers generate collision events but don't affect motion.
IsSleeping¶
Tag indicating a body is currently sleeping (performance optimization):
// Check if sleeping
if (entity.has<engine::physics::IsSleeping>()) {
std::cout << "Body is sleeping" << std::endl;
}
Physics engines automatically sleep inactive bodies.
Collision Events¶
CollisionEvents Component¶
Collision information is stored in the CollisionEvents component:
CollisionInfo:
struct CollisionInfo {
flecs::entity other_entity; // The other entity in collision
glm::vec3 contact_point; // World-space contact point
glm::vec3 contact_normal; // Normal pointing from this to other
float penetration_depth; // How deep the overlap is
bool is_trigger; // True if either body is a trigger
};
Example:
// Query entities with collision events
eng.ecs.GetWorld().each([](flecs::entity e,
const engine::physics::CollisionEvents& events) {
for (const auto& collision : events.events) {
std::cout << "Entity " << e.name()
<< " collided with " << collision.other_entity.name()
<< std::endl;
if (collision.is_trigger) {
std::cout << " Trigger collision!" << std::endl;
}
}
});
Physics World Configuration¶
Configure global physics settings via the PhysicsWorldConfig singleton:
struct PhysicsWorldConfig {
glm::vec3 gravity{0.0f, -9.81f, 0.0f};
float fixed_timestep{1.0f / 60.0f}; // 60 Hz simulation
int max_substeps{4};
bool enable_sleeping{true};
bool show_debug_physics{false}; // Render collision shapes
};
Example:
// Get or create singleton
auto config_entity = eng.ecs.GetWorld().entity("PhysicsConfig");
config_entity.set<engine::physics::PhysicsWorldConfig>({
.gravity = {0.0f, -20.0f, 0.0f}, // Stronger gravity
.fixed_timestep = 1.0f / 120.0f, // 120 Hz simulation
.show_debug_physics = true // Show debug rendering
});
Raycasting¶
Perform raycasts to detect objects along a line:
// Get physics backend
auto backend_ptr = eng.ecs.GetWorld()
.get<engine::physics::PhysicsBackendPtr>();
if (backend_ptr && backend_ptr->backend) {
// Cast ray
engine::physics::Ray ray{
.origin = {0.0f, 0.0f, 0.0f},
.direction = {0.0f, -1.0f, 0.0f}, // Down
.max_distance = 100.0f
};
auto result = backend_ptr->backend->Raycast(ray);
if (result.hit) {
std::cout << "Hit entity: " << result.entity.name() << std::endl;
std::cout << "Distance: " << result.distance << std::endl;
std::cout << "Hit point: " << glm::to_string(result.hit_point) << std::endl;
}
}
RaycastResult:
struct RaycastResult {
bool hit{false};
flecs::entity entity; // Entity that was hit
glm::vec3 hit_point{0.0f};
glm::vec3 hit_normal{0.0f};
float distance{0.0f};
};
Complete Example¶
Here's a complete example setting up a simple physics scene:
import engine;
import glm;
void SetupPhysicsScene(engine::Engine& eng) {
// Import physics backend
eng.ecs.GetWorld().import<engine::physics::JoltPhysicsModule>();
// Configure physics world
auto config = eng.ecs.GetWorld().entity("PhysicsConfig");
config.set<engine::physics::PhysicsWorldConfig>({
.gravity = {0.0f, -9.81f, 0.0f},
.enable_sleeping = true
});
// Create static ground plane
auto ground = eng.ecs.CreateEntity("Ground");
ground.set<engine::components::Transform>({{0.0f, -1.0f, 0.0f}});
ground.set<engine::physics::RigidBody>({
.motion_type = engine::physics::MotionType::Static
});
ground.set<engine::physics::CollisionShape>({
.type = engine::physics::ShapeType::Box,
.box_half_extents = {50.0f, 0.5f, 50.0f} // Large flat box
});
// Create dynamic box
auto box = eng.ecs.CreateEntity("Box");
box.set<engine::components::Transform>({{0.0f, 10.0f, 0.0f}});
box.set<engine::physics::RigidBody>({
.motion_type = engine::physics::MotionType::Dynamic,
.mass = 10.0f,
.friction = 0.5f,
.restitution = 0.3f
});
box.set<engine::physics::CollisionShape>({
.type = engine::physics::ShapeType::Box,
.box_half_extents = {1.0f, 1.0f, 1.0f}
});
// Create character with capsule
auto character = eng.ecs.CreateEntity("Character");
character.set<engine::components::Transform>({{5.0f, 5.0f, 0.0f}});
character.set<engine::physics::RigidBody>({
.motion_type = engine::physics::MotionType::Dynamic,
.mass = 70.0f, // kg
.friction = 0.8f,
.use_gravity = true
});
character.set<engine::physics::CollisionShape>({
.type = engine::physics::ShapeType::Capsule,
.capsule_radius = 0.5f,
.capsule_height = 2.0f,
.offset = {0.0f, 1.0f, 0.0f}
});
// Create trigger zone
auto trigger = eng.ecs.CreateEntity("TriggerZone");
trigger.set<engine::components::Transform>({{0.0f, 2.0f, 0.0f}});
trigger.set<engine::physics::RigidBody>({
.motion_type = engine::physics::MotionType::Static
});
trigger.set<engine::physics::CollisionShape>({
.type = engine::physics::ShapeType::Box,
.box_half_extents = {5.0f, 5.0f, 5.0f}
});
trigger.add<engine::physics::IsTrigger>();
}
Character Controller Pattern¶
For character movement, combine physics with custom control:
void UpdateCharacter(engine::Engine& eng, float delta_time) {
eng.ecs.GetWorld().each([delta_time](
flecs::entity e,
engine::physics::PhysicsVelocity& velocity,
const engine::physics::RigidBody& body
) {
if (e.name() != "Character") return;
// Get input
glm::vec3 move_dir{0.0f};
if (engine::input::Input::IsKeyPressed(engine::input::KeyCode::W)) {
move_dir.z -= 1.0f;
}
if (engine::input::Input::IsKeyPressed(engine::input::KeyCode::S)) {
move_dir.z += 1.0f;
}
if (engine::input::Input::IsKeyPressed(engine::input::KeyCode::A)) {
move_dir.x -= 1.0f;
}
if (engine::input::Input::IsKeyPressed(engine::input::KeyCode::D)) {
move_dir.x += 1.0f;
}
// Normalize and scale
if (glm::length(move_dir) > 0.0f) {
move_dir = glm::normalize(move_dir) * 5.0f; // 5 m/s
}
// Apply to horizontal velocity only (preserve vertical)
velocity.linear.x = move_dir.x;
velocity.linear.z = move_dir.z;
// Jump
if (engine::input::Input::IsKeyJustPressed(engine::input::KeyCode::SPACE)) {
e.set<engine::physics::PhysicsImpulse>({
.impulse = {0.0f, 300.0f, 0.0f}
});
}
});
}
Best Practices¶
- Use appropriate motion types: Static for terrain, Dynamic for interactive objects
- Enable CCD for fast objects: Set
enable_ccd = truefor bullets, fast projectiles - Tune damping values: Prevent bouncing with higher damping
- Use collision layers: Filter what objects collide with each other
- Prefer impulses for instant changes: Use forces for continuous application
- Check collision events: React to collisions via
CollisionEventscomponent - Scale masses realistically: Human ~70kg, car ~1000kg
Performance Tips¶
- Use sleeping: Enable sleeping for better performance with many static bodies
- Simplify collision shapes: Use boxes/spheres instead of complex meshes
- Fixed timestep: Keep
fixed_timestepat 1/60 or 1/120 for stability - Limit max substeps: Prevents simulation spiral of death
Further Reading¶
- Architecture Overview - How physics integrates with ECS
- Getting Started - Set up your project
- Scene Management - Save/load physics setups