角色控制器与载具
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,落地后清零)。
示例
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() | 车轮数量 |
示例
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);
