<Vehicle> drives its children like a toy car on the ground plane (y = 0): throttle and steering go in; position, heading, speed and wheel motion come out. The model is arcade on purpose: responsive, no skidding, no tipping over. The scene renders only while the car moves or an input is held.
import { Car, ContactShadow, Ground, Trees } from "@apricot/assets";
import { FollowCamera, Scene, type SceneObject, Vehicle } from "@apricot/react";
import { useRef } from "react";
import { View } from "react-native";
import { Joystick, useJoystickValues } from "./Joystick";
export function Drive() {
const car = useRef<SceneObject>(null);
const { throttle, steer } = useJoystickValues();
return (
<View style={{ flex: 1 }}>
<Scene style={{ flex: 1 }} look="soft" fog camera={{ position: [0, 2.6, -6], fov: 55 }}>
<Ground />
<Trees count={140} seed={3} />
<Vehicle ref={car} throttle={throttle} steer={steer} bounds={[-90, -90, 90, 90]}>
<Car />
<ContactShadow width={1.5} length={2.5} opacity={0.4} />
</Vehicle>
<FollowCamera target={car} />
</Scene>
<Joystick throttle={throttle} steer={steer} style={{ position: "absolute", bottom: 40, alignSelf: "center" }} />
</View>
);
}Joystick and useJoystickValues come from the drive-joystick recipe: copy that file into your app.
Inputs#
| Prop | Range | Meaning |
|---|---|---|
throttle |
−1 to 1 | 1 full throttle forwards; negative brakes, then reverses |
steer |
−1 to 1 | −1 full left, 1 full right |
Both take a plain number or a shared value. Shared values are read every frame without React renders: use them for joysticks, sliders and gestures. Plain numbers suit buttons and key presses (throttle={gas ? 1 : 0}).
The car#
Anything inside <Vehicle> moves with it. Descendants whose names start with wheel_f turn with the steering and spin; those starting with wheel_r spin.
-
Built-in:
<Car color glass>from@apricot/assetsis a toy car sized for the defaults (wheel radius 0.3, wheel base 1.4), with wheel nodes already named. -
Your own
.glb: use<Model src>inside<Vehicle>. Prepare it so that:- it faces +z, y up, in metres, with the wheels touching y = 0;
- the wheel nodes are named
wheel_fl,wheel_fr,wheel_rl,wheel_rr, each with its origin at the wheel's centre (they turn around their own origin: x for spin, y for steering); - it has been through
npx apricot-models(see Loading models).
Then set
wheelRadius(andwheelBase, the distance between the axles) to match it, so the wheels roll without sliding.
import { Vehicle, type VehicleProps } from "@apricot/react";
import { Model } from "@apricot/react/model";
export const MyCar = (props: VehicleProps) => (
<Vehicle wheelRadius={0.34} wheelBase={2.7} maxSpeed={20} {...props}>
<Model src={require("./assets/models/sedan.glb")} />
</Vehicle>
);Handling#
The options are read once, at mount. To change them, remount (change the key).
| Option | Default | Meaning |
|---|---|---|
maxSpeed |
12 |
Top speed forwards, units per second |
maxReverse |
4 |
Top speed in reverse |
acceleration |
9 |
At full throttle, units per second² |
braking |
20 |
Deceleration with throttle against the motion |
drag |
3 |
Deceleration when coasting |
wheelBase |
1.4 |
Distance between the axles: shorter turns tighter |
maxSteer |
0.6 |
Largest front-wheel angle, radians |
steerSpeed |
8 |
How fast the wheels reach the requested angle, per second |
wheelRadius |
0.3 |
For the wheels' spin |
bounds |
none | [minX, minZ, maxX, maxZ]: keeps the car inside this box |
start |
{ x: 0, z: 0, heading: 0 } |
Start place and heading (radians; 0 faces +z) |
Following it#
<FollowCamera target={ref}> chases the car from behind and above. Put it after the <Vehicle> so it follows this frame's position, not the last one. See Camera. <CameraRig follow={ref}> also works, for an orbiting view instead of a chase view.
Reading the car's state#
onUpdate is called after every simulated frame, on the JS thread, with a VehicleState:
| Field | Meaning |
|---|---|
x, z |
Position on the ground |
heading |
Radians about +y; 0 faces +z |
speed |
Signed speed along the heading, units per second |
steer |
Current front-wheel angle, radians (positive steers right) |
wheelSpin |
Accumulated wheel rotation, radians |
Do not set React state every frame from it. Keep values in a ref, or throttle updates:
import { Car, Ground } from "@apricot/assets";
import { FollowCamera, Scene, type SceneObject, Vehicle, type VehicleState } from "@apricot/react";
import { useRef, useState } from "react";
import { Text, View } from "react-native";
export function Speedometer({ throttle, steer }: { throttle: number; steer: number }) {
const car = useRef<SceneObject>(null);
const [kmh, setKmh] = useState(0);
const onUpdate = (s: Readonly<VehicleState>) => {
const next = Math.round(Math.abs(s.speed) * 3.6);
if (next !== kmh) setKmh(next);
};
return (
<View style={{ flex: 1 }}>
<Scene style={{ flex: 1 }} look="soft">
<Ground />
<Vehicle ref={car} throttle={throttle} steer={steer} onUpdate={onUpdate}>
<Car />
</Vehicle>
<FollowCamera target={car} />
</Scene>
<Text style={{ position: "absolute", top: 60, alignSelf: "center", fontSize: 32 }}>{kmh} km/h</Text>
</View>
);
}Related#
- drive-joystick recipe.
- Motion reference.
- Assets:
Car,Ground,Trees,ContactShadow.