Character Controllers

Implement physics-based character movement:

// Create a character with capsule physics
const characterHeight = 3;
const characterRadius = 0.5;

// Create body parts
const body = BABYLON.MeshBuilder.CreateCylinder("body", {
    height: characterHeight - 2 * characterRadius,
    diameter: characterRadius * 2
}, scene);

const head = BABYLON.MeshBuilder.CreateSphere("head", {
    diameter: characterRadius * 2
}, scene);
head.position.y = (characterHeight - 2 * characterRadius) / 2;
head.parent = body;

const feet = BABYLON.MeshBuilder.CreateSphere("feet", {
    diameter: characterRadius * 2
}, scene);
feet.position.y = -(characterHeight - 2 * characterRadius) / 2;
feet.parent = body;

// Position the character
body.position.y = characterHeight / 2;

// Add physics with capsule shape (cylinder + sphere caps)
body.physicsImpostor = new BABYLON.PhysicsImpostor(
    body,
    BABYLON.PhysicsImpostor.CylinderImpostor,
    { mass: 1, friction: 0.5, restitution: 0.3 },
    scene
);

// Variables for movement control
const moveDirection = new BABYLON.Vector3();
let isGrounded = false;
const jumpForce = 10;
const moveSpeed = 0.5;

// Input handling for character movement
const keyStatus = {};

scene.onKeyboardObservable.add((kbInfo) => {
    const key = kbInfo.event.key.toLowerCase();
    
    if (kbInfo.type === BABYLON.KeyboardEventTypes.KEYDOWN) {
        keyStatus[key] = true;
    } else if (kbInfo.type === BABYLON.KeyboardEventTypes.KEYUP) {
        keyStatus[key] = false;
    }
});

// Ground detection using raycasting
function checkGrounded() {
    const origin = body.getAbsolutePosition();
    const direction = new BABYLON.Vector3(0, -1, 0);
    const length = characterHeight / 2 + 0.1; // Slightly longer than character radius
    
    const raycastResult = scene.getPhysicsEngine().raycast(
        origin,
        direction,
        length
    );
    
    return raycastResult.hasHit;
}

// Character controller update
scene.onBeforeRenderObservable.add(() => {
    // Check if character is on ground
    isGrounded = checkGrounded();
    
    // Reset movement direction
    moveDirection.setAll(0);
    
    // Calculate move direction from input
    if (keyStatus["w"]) moveDirection.z += 1;
    if (keyStatus["s"]) moveDirection.z -= 1;
    if (keyStatus["a"]) moveDirection.x -= 1;
    if (keyStatus["d"]) moveDirection.x += 1;
    
    // Jump if on ground
    if (keyStatus[" "] && isGrounded) {
        body.physicsImpostor.applyImpulse(
            new BABYLON.Vector3(0, jumpForce, 0),
            body.getAbsolutePosition()
        );
    }
    
    // Normalize and apply movement
    if (moveDirection.length() > 0) {
        moveDirection.normalize();
        
        // Align with camera direction
        const cameraDirection = camera.getTarget().subtract(camera.position);
        cameraDirection.y = 0;
        cameraDirection.normalize();
        
        // Create rotation matrix from camera direction
        const rotationMatrix = BABYLON.Matrix.RotationY(
            Math.atan2(cameraDirection.x, cameraDirection.z)
        );
        
        // Transform movement direction relative to camera
        const transformedDirection = BABYLON.Vector3.TransformNormal(
            moveDirection,
            rotationMatrix
        );
        
        // Apply movement as impulse (only when on ground)
        if (isGrounded) {
            body.physicsImpostor.applyImpulse(
                transformedDirection.scale(moveSpeed),
                body.getAbsolutePosition()
            );
        }
    }
});