Examples
Wiki page
Character Controller Pattern
A practical rigidbody/capsule character controller recipe using the current public physics API.
Status
This is a practical pattern derived from the current public API, not an existing checked-in sample file. Use it as a starting point for a gameplay controller.
The recipe uses:
TransformRigidbodyCapsuleCollider- custom
CharacterController InputManagerPhysicsSystem::GetLinearVelocityPhysicsSystem::SetLinearVelocityPhysicsSystem::AddImpulsePhysicsSystem::Raycast
Scene Entity
{
"id": 10,
"components": [
{
"type": "Transform",
"data": {
"position": [0.0, 2.0, 0.0],
"rotation": [0.0, 0.0, 0.0, 1.0],
"scale": [1.0, 1.0, 1.0]
}
},
{
"type": "Rigidbody",
"data": {
"type": "Dynamic",
"mass": 1.0,
"useGravity": true,
"lockRotation": [true, true, true],
"lockMovement": [false, false, false]
}
},
{
"type": "CapsuleCollider",
"data": {
"center": [0.0, 0.9, 0.0],
"radius": 0.35,
"height": 1.8,
"friction": 0.2,
"restitution": 0.0,
"isTrigger": false
}
},
{
"type": "CharacterController",
"data": {
"speed": 6.0,
"jumpImpulse": 5.5,
"groundProbeDistance": 1.05
}
}
]
}Locking rotation keeps the character upright. The capsule center/height should match your visual character.
Component
struct CharacterController {
float speed = 6.0f;
float jumpImpulse = 5.5f;
float groundProbeDistance = 1.05f;
};Creator
inline void CreateCharacterControllerComponent(
Public::ECS::EntityID entity,
const json& data
) {
CharacterController controller{};
controller.speed = data.value("speed", controller.speed);
controller.jumpImpulse = data.value("jumpImpulse", controller.jumpImpulse);
controller.groundProbeDistance =
data.value("groundProbeDistance", controller.groundProbeDistance);
Public::Engine::getInstance()
.GetCoordinator()
.addComponent(entity, controller);
}Register it before Initialize():
engine.RegisterComponent("CharacterController", CreateCharacterControllerComponent);System Signature
Public::ECS::ComponentSignature signature;
signature.set(coord.getComponentType<Public::ECS::Components::Transform>());
signature.set(coord.getComponentType<Public::ECS::Components::Rigidbody>());
signature.set(coord.getComponentType<Public::ECS::Components::CapsuleCollider>());
signature.set(coord.getComponentType<CharacterController>());
coord.setSystemSignature<CharacterControllerSystem>(signature);Ground Check
Use a downward raycast from the character position:
bool CharacterControllerSystem::IsGrounded(
Public::ECS::EntityID entity,
const Public::ECS::Components::Transform& transform,
const CharacterController& controller
) {
auto* physics = engine->GetPhysicsSystem();
if (!physics) {
return false;
}
float origin[3] = {
transform.position.x,
transform.position.y + 0.05f,
transform.position.z
};
float direction[3] = {0.0f, -1.0f, 0.0f};
Public::ECS::Structs::PhysicsRaycastHit hit;
return physics->Raycast(origin, direction, controller.groundProbeDistance, hit) &&
hit.hit;
}If your ray hits the character itself, offset the origin or filter using simulation setup. The current public raycast returns a body handle, not an entity ID.
Movement Update
Preserve vertical velocity and replace horizontal velocity:
void CharacterControllerSystem::update(float dt) {
auto* physics = engine->GetPhysicsSystem();
if (!physics || !input->IsWindowFocused()) {
return;
}
for (auto entity : entityList()) {
auto transformOpt = componentManager->getComponent<Transform>(entity);
auto controllerOpt = componentManager->getComponent<CharacterController>(entity);
if (!transformOpt || !controllerOpt) {
continue;
}
auto& transform = transformOpt->get();
auto& controller = controllerOpt->get();
float moveX = 0.0f;
float moveZ = 0.0f;
if (input->IsKeyDown(Public::Input::Key::Code::A)) moveX -= 1.0f;
if (input->IsKeyDown(Public::Input::Key::Code::D)) moveX += 1.0f;
if (input->IsKeyDown(Public::Input::Key::Code::W)) moveZ += 1.0f;
if (input->IsKeyDown(Public::Input::Key::Code::S)) moveZ -= 1.0f;
Eigen::Vector3f move(moveX, 0.0f, moveZ);
if (move.squaredNorm() > 0.0f) {
move.normalize();
}
float velocity[3] = {0.0f, 0.0f, 0.0f};
if (physics->GetLinearVelocity(entity, velocity)) {
velocity[0] = move.x() * controller.speed;
velocity[2] = move.z() * controller.speed;
physics->SetLinearVelocity(entity, velocity);
}
if (input->GetKeyDown(Public::Input::Key::Code::Space) &&
IsGrounded(entity, transform, controller)) {
float impulse[3] = {0.0f, controller.jumpImpulse, 0.0f};
physics->AddImpulse(entity, impulse);
}
}
}Practical Tuning
- Use
CapsuleColliderfor upright characters. - Lock rotation on all axes.
- Keep
useGravity = true. - Lower collider friction if movement sticks on slopes or edges.
- Use
SetLinearVelocityfor direct walking control. - Use
AddImpulsefor jumps. - Use raycasts for grounded checks.
- Avoid writing
Transform.positiondirectly for dynamic bodies during normal movement; let physics own body motion.
