️ A physics-driven controller toolkit for React Three Fiber + Rapier
️ A physics-driven controller toolkit for React Three Fiber + Rapier
Physics-driven character, vehicle, drone, and custom-gravity controllers for React Three Fiber and Rapier.
Ecctrl is a modular controller toolkit for custom-gravity worlds, ShapeCast characters, torque-driven cars, thrust-driven drones, touch controls, runtime animation states, and editable physics curves.
Explore Ecctrl at ecctrl.app.
This is the best way to test the character controller, custom gravity, cars, drones, touch controls, time control, and tuning tools before adding Ecctrl to your project.
npm install ecctrlPeer dependencies:
npm install three react react-dom @react-three/fiber @react-three/rapierOptional packages:
npm install @react-three/drei leva@react-three/drei is only needed for helpers such as KeyboardControls and camera tooling. If you do not use Drei, you can still drive controller input through refs or stores and build your own camera follow logic. leva is only needed if you want the visual curve editor or debug panels.
This is the smallest useful ShapeCast character setup:
import { KeyboardControls } from "@react-three/drei";
import { Physics } from "@react-three/rapier";
import { Ecctrl } from "ecctrl";
const keyboardMap = [
{ name: "forward", keys: ["ArrowUp", "KeyW"] },
{ name: "backward", keys: ["ArrowDown", "KeyS"] },
{ name: "leftward", keys: ["ArrowLeft", "KeyA"] },
{ name: "rightward", keys: ["ArrowRight", "KeyD"] },
{ name: "jump", keys: ["Space"] },
{ name: "run", keys: ["Shift"] },
];
export function CharacterScene() {
return (
);
}For custom gravity scenes, set Rapier gravity to [0, 0, 0] and let Ecctrl apply gravity per body:
The README gives the main concepts and common setup paths. Full prop tables, config defaults, handle output, input shapes, and tuning notes are available in the detailed docs:
Ecctrl is built for games and simulations where controlled objects need to behave like part of the physics world.
The character floats with spring and damping, pushes back on the object below, follows moving platforms, aligns with custom gravity, and exposes runtime state for animation and gameplay logic. Ecctrl keeps that physics-driven direction and extends it into cars, drones, touch input, time control, and editable curve-driven behavior.
| Feature | What it gives you |
|---|---|
| Custom gravity | Position-based gravity fields shared by characters, vehicles, drones, and custom rigid bodies |
| ShapeCast character | Stable floating support, accurate standing points, gap handling, slope checks, and optional RayCast mode |
| ShapeCast wheels | Torque-driven wheel simulation with editable longitudinal and lateral slip curves |
| Propeller drones | Thrust and reaction torque are mixed per propeller for stabilized velocity or position flight |
| Curve LUTs | Runtime-cheap curve sampling for grip, engine torque, steering, and mass falloff |
| Leva curve editor | Optional draggable curve editor for tuning points, tangents, and weights |
| Time control | Pause and slow down the physics world for bullet-time style control |
| Touch controls | DOM-based joystick and virtual buttons that can be replaced by your own UI |
Ecctrl is split into subpath exports so you can import only the parts you need.
import { Ecctrl } from "ecctrl";
import { EcctrlVehicle, ShapeCastWheel, ThrustPropeller } from "ecctrl/vehicle";
import { Joystick, VirtualButton } from "ecctrl/input";
import { useCustomGravity } from "ecctrl/gravity";
import { EcctrlCameraControls } from "ecctrl/camera";
import { TimeControl } from "ecctrl/time";
import { EcctrlAnimationStateController } from "ecctrl/animation";
import { bakeCurveLUT, evaluateCurveLUT } from "ecctrl/curves";
import { CurveEditorPlugin } from "ecctrl/leva";For prototypes or examples, you can also import everything from ecctrl/all.
Custom gravity is one of the main systems in Ecctrl.
Instead of relying only on Rapier's global gravity vector, Ecctrl can read a gravity field function and apply gravity per body. This makes it possible to build spherical gravity, cylindrical gravity, wall-walking zones, gravity tunnels, rotating gravity fields, or gameplay-specific attraction areas.
When Ecctrl applies custom gravity, set Rapier's global gravity to [0, 0, 0] so the global gravity force and custom gravity field do not stack.
…For custom rigid bodies outside Ecctrl, call applyGravityField each frame:
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { RigidBody, useRapier, type RapierRigidBody } from "@react-three/rapier";
import { useCustomGravity } from "ecctrl/gravity";
function CustomGravityRigidBody() {
const body = useRef(null);
const { world } = useRapier();
const applyGravityField = useCustomGravity((state) => state.applyGravityField);
useFrame(() => {
if (body.current) applyGravityField(body.current, world.timestep);
});
return ;
}For dynamic gravity, keep the field function stable and update refs that the function reads. This avoids React renders and Zustand updates on every frame.
const gravityStrength = useRef(9.81);
const gravityTarget = useRef(new THREE.Vector3(0, 20, 0));
useEffect(() => {
setGravityField((bodyPos) => gravity.subVectors(gravityTarget.current, bodyPos).normalize().multiplyScalar(gravityStrength.current));
}, [setGravityField]);
useFrame(({ clock }) => {
// Move the attractor without replacing the gravity field function.
const t = clock.elapsedTime;
gravityTarget.current.set(Math.sin(t) * 10, 20, Math.cos(t) * 10);
});EcctrlCameraControls extends Drei CameraControls with setUp(newUp). It does not automatically follow a character by itself; you still drive the follow target from your controller ref.
…The character controller now uses ShapeCast ground detection by default. RayCast mode is still available when you want the lowest possible detection cost.
For the minimum setup, see Quick Start.
ShapeCast mode gives the character a stronger ground signal than a single ray:
groundDetection="rayCast" mode for lower-cost scenesThe character can push back on the world instead of only moving itself:
These behaviors are enabled by default. You can disable them when a scene needs simpler interaction:
EcctrlHandle exposes readonly runtime state for animation, gameplay logic, debug UI, or user-defined systems.
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { Ecctrl, type EcctrlHandle } from "ecctrl";
const ecctrl = useRef(null);
useFrame(() => {
if (!ecctrl.current) return;
const grounded = ecctrl.current.isOnGround;
const speed = ecctrl.current.moveSpeed;
const falling = ecctrl.current.isFalling;
});
EcctrlHandle exposes more runtime values than the three shown here, including position, velocity, gravity direction, input state, and body axes. See the detailed docs for the full handle shape.
Ecctrl includes a lightweight animation state resolver. The controller writes the current state into useEcctrlAnimationStore; your model decides how to play, blend, or hold each animation clip.
Default states:
"IDLE" | "WALK" | "RUN" | "JUMP_START" | "JUMP_IDLE" | "JUMP_FALL" | "JUMP_LAND"Basic setup:
…For custom state mapping, pass a resolver:
import {
EcctrlAnimationStateController,
resolveEcctrlAnimationState,
type EcctrlAnimationStateResolver,
} from "ecctrl/animation";
const resolver: EcctrlAnimationStateResolver = (ctx) => {
if (ctx.handle.moveSpeed > 8) return "RUN";
return resolveEcctrlAnimationState(ctx);
};
;For fully custom animation graphs, skip the store and read EcctrlHandle directly in your animation system.
Ecctrl includes a vehicle controller that can be built from wheels, propellers, or both.
import { EcctrlVehicle, ShapeCastWheel } from "ecctrl/vehicle";
import { CuboidCollider } from "@react-three/rapier";
EcctrlVehicle owns the body state, input state, custom gravity, and high-level control logic. Wheels and propellers register as modules, compute local wheel or propeller data, and feed the vehicle with the values it needs to apply impulses.
Driving input can come from keyboard controls, touch controls, AI, or your own code:
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import { EcctrlVehicle, type EcctrlVehicleHandle, type VehicleInput } from "ecctrl/vehicle";
function VehicleInputExample({ input }: { input: VehicleInput }) {
const vehicle = useRef(null);
useFrame(() => {
vehicle.current?.setMovement(input);
});
return (
{/* body collider and vehicle modules */}
);
}ShapeCastWheel is designed for physics-driven vehicle control.
The wheel receives drive torque, updates wheel angular velocity, evaluates slip through baked curves, then applies longitudinal and lateral grip impulses to the vehicle body.
torque -> wheel speed -> slip -> grip -> impulseKey features:
groundDetection="rayCast" mode for maximum performanceNo open issues yet, or sync has not completed.