Skip to content

角色控制器与载具

Rapier 版内置了两个高层组件:用于第一/第三人称移动的 角色控制器 CharacterController,以及用于车辆模拟的 载具控制器 VehicleController

角色控制器(CharacterController)

角色控制器是一个运动学控制器,自带爬坡、台阶、贴地等处理,适合做可走动的玩家角色。通过 controller.move(displacement) 驱动移动:

ts
import { Object3D, Vector3 } from '@orillusion/core';
import { CharacterController } from '@orillusion/physics-rapier';

const player = new Object3D();
const cc = player.addComponent(CharacterController);
cc.maxSlopeClimbAngle = (50 * Math.PI) / 180; // 最大可爬坡角(弧度)
cc.snapToGround = 0.5;                          // 贴地吸附距离
scene.addChild(player);

// 在每帧逻辑中按输入驱动移动(位移向量,已含重力/跳跃分量)
cc.move(new Vector3(vx * speed * dt, vy * dt, vz * speed * dt));
成员说明
move(displacement)以位移向量驱动角色(自动处理碰撞滑动、爬坡、台阶)
maxSlopeClimbAngle最大可攀爬坡度(弧度)
snapToGround下台阶时的贴地吸附距离

重力与跳跃需自行在位移的 Y 分量中累加(积累竖直速度 vy,落地后清零)。

示例

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { View3D, Scene3D, CameraUtil, HoverCameraController, DirectLight, AtmosphericComponent, Object3D, LitMaterial, Engine3D, BoxGeometry, MeshRenderer, Vector3, PlaneGeometry, Color, KeyEvent, KeyCode, ComponentBase, Time } from "@orillusion/core";
import { Physics, Rigidbody, BodyType, CollisionShapeUtil, CharacterController, RAPIER } from "@orillusion/physics-rapier";

class CharacterDriver extends ComponentBase {
    public controller!: CharacterController;
    private keys: Record<string, boolean> = {};
    public speed: number = 4;
    public gravity: number = -9.8;
    public jumpVel: number = 6;
    private vy: number = 0;

    public start() {
        const input = this.transform.scene3D.view.engine3D.inputSystem;
        input.addEventListener(KeyEvent.KEY_DOWN, (e: KeyEvent) => this.keys[e.keyCode] = true, this);
        input.addEventListener(KeyEvent.KEY_UP, (e: KeyEvent) => this.keys[e.keyCode] = false, this);
    }

    public onUpdate() {
        if (!this.controller) return;
        const dt = Time.delta * 0.001;
        let vx = 0, vz = 0;
        if (this.keys[KeyCode.Key_W]) vz -= 1;
        if (this.keys[KeyCode.Key_S]) vz += 1;
        if (this.keys[KeyCode.Key_A]) vx -= 1;
        if (this.keys[KeyCode.Key_D]) vx += 1;
        const len = Math.hypot(vx, vz);
        if (len > 0) { vx /= len; vz /= len; }

        // Apply gravity
        if (this.controller.isGrounded()) {
            this.vy = 0;
            if (this.keys[KeyCode.Key_Space]) this.vy = this.jumpVel;
        } else {
            this.vy += this.gravity * dt;
        }

        this.controller.move(new Vector3(vx * this.speed * dt, this.vy * dt, vz * this.speed * dt));
    }
}

class Sample_RapierCharacter {
    async run() {
        await Physics.init();
        const engine = await Engine3D.init({ renderLoop: () => Physics.update() });
        let scene = new Scene3D();

        // Setup camera
        let camera = CameraUtil.createCamera3DObject(scene);
        camera.perspective(60, engine.aspect, 0.1, 800.0);

        let hoverCtrl = camera.object3D.addComponent(HoverCameraController);
        hoverCtrl.setCamera(0, -25, 15);
        hoverCtrl.dragSmooth = 4;

        // Create directional light
        let lightObj3D = new Object3D();
        lightObj3D.localRotation = new Vector3(-35, -143, 92);

        let light = lightObj3D.addComponent(DirectLight);
        light.lightColor = Color.COLOR_WHITE;
        light.castShadow = true;
        light.enableCSM = true;
        light.intensity = 2.2;
        scene.addChild(light.object3D);

        // init sky
        let atmosphericSky = scene.addComponent(AtmosphericComponent);
        atmosphericSky.sunY = 0.6;

        // Floor
        const floor = new Object3D();
        const fr = floor.addComponent(MeshRenderer);
        fr.geometry = new PlaneGeometry(60, 60);
        const fm = new LitMaterial(); fm.baseColor = new Color(0.4, 0.4, 0.45); fm.roughness = 0.8; fr.material = fm;
        const fb = floor.addComponent(Rigidbody);
        fb.bodyType = BodyType.Static; fb.shape = CollisionShapeUtil.createPlaneShape(30, 0.05);
        scene.addChild(floor);

        // Stairs
        for (let i = 0; i < 6; i++) {
            const step = new Object3D(); step.x = 8; step.y = 0.25 + i * 0.5; step.z = -i * 1.2;
            const mr = step.addComponent(MeshRenderer);
            mr.geometry = new BoxGeometry(3, 0.5, 1.2);
            const mat = new LitMaterial(); mat.baseColor = new Color(0.6, 0.5, 0.4); mr.material = mat;
            const rb = step.addComponent(Rigidbody);
            rb.bodyType = BodyType.Static; rb.shape = CollisionShapeUtil.createBoxShape(step, new Vector3(3, 0.5, 1.2));
            scene.addChild(step);
        }

        // Character: blue capsule (rendered as box)
        const player = new Object3D(); player.y = 2;
        const pmr = player.addComponent(MeshRenderer);
        pmr.geometry = new BoxGeometry(0.6, 1.6, 0.6);
        const pm = new LitMaterial(); pm.baseColor = new Color(0.3, 0.5, 0.9); pmr.material = pm;
        const cc = player.addComponent(CharacterController);
        cc.shape = RAPIER.ColliderDesc.capsule(0.6, 0.3);
        cc.maxSlopeClimbAngle = (50 * Math.PI) / 180;
        cc.snapToGround = 0.5;
        cc.autoStep = { maxHeight: 0.6, minWidth: 0.2, includeDynamic: true };
        const drv = player.addComponent(CharacterDriver);
        drv.controller = cc;
        scene.addChild(player);

        let view = new View3D();
        view.camera = camera;
        view.scene = scene;
        engine.startRenderView(view);
    }
}

new Sample_RapierCharacter().run();

载具控制器(VehicleController)

载具控制器基于射线车轮模型:给底盘刚体添加 VehicleController,再逐个 addWheel 添加车轮,运行时通过 setEngineForce / setBrake / setSteering 控制。

ts
import { Object3D, Vector3 } from '@orillusion/core';
import { Rigidbody, BodyType, CollisionShapeUtil, VehicleController } from '@orillusion/physics-rapier';

// 底盘(动态刚体)
const chassis = new Object3D();
chassis.y = 2;
const crb = chassis.addComponent(Rigidbody);
crb.bodyType = BodyType.Dynamic;
crb.shape = CollisionShapeUtil.createBoxShape(chassis, new Vector3(2, 0.6, 4));
scene.addChild(chassis);

// 载具控制器 + 四个车轮
const vc = chassis.addComponent(VehicleController);
const wheelOpts = {
    suspensionRestLength: 0.3,
    radius: 0.4,
    stiffness: 30,
    dampingCompression: 4.4,
    dampingRelaxation: 2.3,
    frictionSlip: 1000,
};
vc.addWheel({ ...wheelOpts, chassisConnection: new Vector3(-1, -0.3, -1.5) }); // 0 前左
vc.addWheel({ ...wheelOpts, chassisConnection: new Vector3( 1, -0.3, -1.5) }); // 1 前右
vc.addWheel({ ...wheelOpts, chassisConnection: new Vector3(-1, -0.3,  1.5) }); // 2 后左
vc.addWheel({ ...wheelOpts, chassisConnection: new Vector3( 1, -0.3,  1.5) }); // 3 后右

运行时控制(按车轮索引):

ts
// 后轮驱动
vc.setEngineForce(engineForce, 2);
vc.setEngineForce(engineForce, 3);
// 前轮转向
vc.setSteering(steerAngle, 0);
vc.setSteering(steerAngle, 1);
// 刹车(所有轮)
for (let i = 0; i < vc.numWheels(); i++) vc.setBrake(brakeForce, i);
方法说明
addWheel(opts)添加一个车轮(含悬挂、半径、连接点等)
setEngineForce(force, wheelIndex)设置某轮的引擎驱动力
setBrake(force, wheelIndex)设置某轮的刹车力
setSteering(angle, wheelIndex)设置某轮的转向角
numWheels()车轮数量

示例

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Scene3D, CameraUtil, HoverCameraController, DirectLight, View3D, AtmosphericComponent, Object3D, LitMaterial, Engine3D, BoxGeometry, CylinderGeometry, MeshRenderer, Vector3, Color, KeyEvent, KeyCode, ComponentBase } from "@orillusion/core";
import { Physics, Rigidbody, BodyType, CollisionShapeUtil, VehicleController } from "@orillusion/physics-rapier";

const RAD2DEG = 180 / Math.PI;

class VehicleDriver extends ComponentBase {
    public vehicle!: VehicleController;
    private keys: Record<string, boolean> = {};
    public engineForce: number = 1500;
    public brakeForce: number = 100;
    public maxSteer: number = 0.5;

    public start() {
        const input = this.transform.scene3D.view.engine3D.inputSystem;
        input.addEventListener(KeyEvent.KEY_DOWN, (e: KeyEvent) => this.keys[e.keyCode] = true, this);
        input.addEventListener(KeyEvent.KEY_UP, (e: KeyEvent) => this.keys[e.keyCode] = false, this);
    }

    public onUpdate() {
        if (!this.vehicle || this.vehicle.numWheels() < 4) return;
        const fwd = this.keys[KeyCode.Key_W] ? this.engineForce : (this.keys[KeyCode.Key_S] ? -this.engineForce : 0);
        const brake = this.keys[KeyCode.Key_Space] ? this.brakeForce : 0;
        const steer = this.keys[KeyCode.Key_A] ? this.maxSteer : (this.keys[KeyCode.Key_D] ? -this.maxSteer : 0);

        // Rear-wheel drive: wheels 2,3
        this.vehicle.setEngineForce(fwd, 2);
        this.vehicle.setEngineForce(fwd, 3);
        for (let i = 0; i < 4; i++) this.vehicle.setBrake(brake, i);
        // Front wheels steer
        this.vehicle.setSteering(steer, 0);
        this.vehicle.setSteering(steer, 1);
    }
}

class WheelVisualSync extends ComponentBase {
    public vehicle!: VehicleController;
    public steerPivots: Object3D[] = [];
    public spinPivots: Object3D[] = [];

    public onUpdate() {
        const ctrl = this.vehicle?.native;
        if (!ctrl) return;
        const n = Math.min(this.steerPivots.length, ctrl.numWheels());
        for (let i = 0; i < n; i++) {
            const conn = ctrl.wheelChassisConnectionPointCs(i);
            const dir = ctrl.wheelDirectionCs(i);
            const len = ctrl.wheelSuspensionLength(i) ?? 0;
            if (!conn || !dir) continue;
            const sp = this.steerPivots[i];
            sp.x = conn.x + dir.x * len;
            sp.y = conn.y + dir.y * len;
            sp.z = conn.z + dir.z * len;
            sp.rotationY = (ctrl.wheelSteering(i) ?? 0) * RAD2DEG;
            this.spinPivots[i].rotationX = (ctrl.wheelRotation(i) ?? 0) * RAD2DEG;
        }
    }
}

class Sample_RapierVehicle {
    async run() {
        await Physics.init();
        const engine = await Engine3D.init({ renderLoop: () => Physics.update() });
        let scene = new Scene3D();

        // Setup camera
        let camera = CameraUtil.createCamera3DObject(scene);
        camera.perspective(60, engine.aspect, 0.1, 800.0);

        let hoverCtrl = camera.object3D.addComponent(HoverCameraController);
        hoverCtrl.setCamera(0, -25, 15);
        hoverCtrl.dragSmooth = 4;

        // Create directional light
        let lightObj3D = new Object3D();
        lightObj3D.localRotation = new Vector3(-35, -143, 92);

        let light = lightObj3D.addComponent(DirectLight);
        light.lightColor = Color.COLOR_WHITE;
        light.castShadow = true;
        light.enableCSM = true;
        light.intensity = 2.2;
        scene.addChild(light.object3D);

        // init sky
        let atmosphericSky = scene.addComponent(AtmosphericComponent);
        atmosphericSky.sunY = 0.6;

        // Ground
        const floor = new Object3D();
        const fr = floor.addComponent(MeshRenderer);
        fr.geometry = new BoxGeometry(200, 0.2, 60);
        const fm = new LitMaterial(); fm.baseColor = new Color(0.35, 0.35, 0.4); fr.material = fm;
        const fb = floor.addComponent(Rigidbody);
        fb.bodyType = BodyType.Static;
        fb.shape = CollisionShapeUtil.createBoxShape(floor, new Vector3(200, 0.2, 60));
        fb.friction = 1.0;
        scene.addChild(floor);

        // Chassis
        const chassis = new Object3D(); chassis.y = 2;
        const cmr = chassis.addComponent(MeshRenderer);
        cmr.geometry = new BoxGeometry(2, 0.6, 4);
        const cm = new LitMaterial(); cm.baseColor = new Color(0.8, 0.2, 0.2); cmr.material = cm;
        const crb = chassis.addComponent(Rigidbody);
        crb.bodyType = BodyType.Dynamic; crb.mass = 200;
        crb.shape = CollisionShapeUtil.createBoxShape(chassis, new Vector3(2, 0.6, 4));
        crb.linearDamping = 0.1;
        crb.angularDamping = 0.5;
        scene.addChild(chassis);

        // Vehicle
        const vc = chassis.addComponent(VehicleController);
        const wheelOpts = {
            suspensionRestLength: 0.3,
            radius: 0.4,
            stiffness: 30,
            dampingCompression: 4.4,
            dampingRelaxation: 2.3,
            frictionSlip: 1000,
        };
        // 4 wheels: front-left, front-right, rear-left, rear-right
        vc.addWheel({ ...wheelOpts, chassisConnection: new Vector3(-1, -0.3, -1.5) }); // 0 FL
        vc.addWheel({ ...wheelOpts, chassisConnection: new Vector3(1, -0.3, -1.5) });  // 1 FR
        vc.addWheel({ ...wheelOpts, chassisConnection: new Vector3(-1, -0.3, 1.5) });  // 2 RL
        vc.addWheel({ ...wheelOpts, chassisConnection: new Vector3(1, -0.3, 1.5) });   // 3 RR

        const drv = chassis.addComponent(VehicleDriver);
        drv.vehicle = vc;

        // Wheel visuals: steerPivot -> spinPivot -> cylinder (rotated to lie along axle)
        const wheelMat = new LitMaterial();
        wheelMat.baseColor = new Color(0.08, 0.08, 0.08);
        const wheelGeo = new CylinderGeometry(wheelOpts.radius, wheelOpts.radius, 0.3, 24);
        const sync = chassis.addComponent(WheelVisualSync);
        sync.vehicle = vc;
        for (let i = 0; i < 4; i++) {
            const steer = new Object3D();
            const spin = new Object3D();
            const mesh = new Object3D();
            const mr = mesh.addComponent(MeshRenderer);
            mr.geometry = wheelGeo;
            mr.material = wheelMat;
            // Cylinder is along Y; rotate Z=90 so it lies along the X axle.
            mesh.rotationZ = 90;
            spin.addChild(mesh);
            steer.addChild(spin);
            chassis.addChild(steer);
            sync.steerPivots.push(steer);
            sync.spinPivots.push(spin);
        }

        let view = new View3D();
        view.camera = camera;
        view.scene = scene;
        engine.startRenderView(view);
    }
}

new Sample_RapierVehicle().run();

调试与拖拽

物理插件还提供了开箱即用的调试线框与鼠标拖拽工具:

ts
import { Graphic3D } from '@orillusion/graphic';

// 实时绘制碰撞体线框
const graphic = new Graphic3D();
scene.addChild(graphic);
Physics.initDebugDrawer(graphic, { enable: true, updateFreq: 1 });

// 允许用鼠标拖拽动态刚体
Physics.enableDragger(view);