Examples

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:

  • Transform
  • Rigidbody
  • CapsuleCollider
  • custom CharacterController
  • InputManager
  • PhysicsSystem::GetLinearVelocity
  • PhysicsSystem::SetLinearVelocity
  • PhysicsSystem::AddImpulse
  • PhysicsSystem::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 CapsuleCollider for upright characters.
  • Lock rotation on all axes.
  • Keep useGravity = true.
  • Lower collider friction if movement sticks on slopes or edges.
  • Use SetLinearVelocity for direct walking control.
  • Use AddImpulse for jumps.
  • Use raycasts for grounded checks.
  • Avoid writing Transform.position directly for dynamic bodies during normal movement; let physics own body motion.