Vehicle

Машина — это два аспекта, каждый на узле, который он описывает:

где что это
Vehicle узел шасси машина целиком: масса, двигатель, руль, аэродинамика — и педали
Wheel узел каждого колеса это колесо: размер, подвеска, шина, его тормоз — и его показания

SDK — это машина как железо: входы (throttle, steer, steerForce, ratio, clutch, brake на каждое колесо) и датчики (rpm, speed, yawRate, driftAngle, по-колёсные slip, load, силы…). Всё, что решает водитель, — дело игры: какая передача, когда схватывает сцепление, может ли двигатель заглохнуть, как руль сужается со скоростью, ABS, ручник, позирование моделей колёс — каждое по несколько строк над этими показаниями. Коробки передач в SDK нет: коробка передач — дело игры.

Разделение повторяет движок: Jolt хранит настройки подвески и шины на колесо, SDK тоже. Узлу шасси нужен Shape (его тело столкновений), и он не должен нести ещё и аспект Physics — Vehicle владеет своим твёрдым телом (прикрепить его рядом бросает исключение), как CharacterController владеет своим контроллером. Контакты по-прежнему работают: car.addEventListener('enter', …) срабатывает как для любого тела.

Обзор

TypeScript
Physics.configure({ gravity: [0, -9.81, 0] })
const scene = new Scene()
const road = Material.lit({ color: '#2b3550' })
const paint = Material.lit({ color: '#c85a3c' })

scene.add(Mesh.box({ material: road, size: [40, 1, 400], position: [0, -0.5, -100] })
  .aspect(Shape, {})
  .aspect(Physics, { motion: 'static', friction: 1 }))    // асфальт: шины читают трение ЗЕМЛИ

const R = 0.35                                           // радиус колеса, м
const hub = (x: number, z: number) => {
  const n = new Node()
  n.position = [x, R - 0.6, z]                           // где центр колеса покоится на ровной земле
  return n.aspect(Wheel, { radius: R, width: 0.25 })
}
const chassis = new Node()
chassis.position = [0, 0.6, 0]
chassis.add(
  Mesh.box({ material: paint, size: [1.8, 0.6, 4.2], position: [0, 0.05, 0] }),
  hub(-0.8, -1.4), hub(0.8, -1.4), hub(-0.8, 1.4), hub(0.8, 1.4),   // вперёд — это −Z: передняя ось
)
const car = chassis.aspect(Shape, { box: [0.9, 0.4, 2.1] }).aspect(Vehicle, { mass: 1200 })
scene.add(car)

class Driver extends System<'driver'> {
  updateBefore() {                                       // РАНО: шаг физики этого кадра едет на этом
    const v = car.vehicle
    const fwd = (Input.key('KeyW') ? 1 : 0) - (Input.key('KeyS') ? 1 : 0)
    v.throttle = Math.abs(fwd)                           // 0..1, без знака
    v.ratio = fwd > 0 ? 2.66 : fwd < 0 ? -2.9 : 0        // передача: её ЗНАК — направление, 0 = нейтраль
    v.steer = (Input.key('KeyD') ? 1 : 0) - (Input.key('KeyA') ? 1 : 0)
    for (const w of v.wheels) w.brake = Input.key('Space') && !w.steer ? 4500 : 0   // Н·м: ручник
  }
}
scene.system(Driver)
scene.open()

Это весь минимум. Vehicle находит каждое Wheel под шасси, группирует их в оси по Z (передняя первой), и умолчания получаются сами: передняя ось рулит, задняя — ведущая. Любое из этого переопределяется на колесо. У машины пока нет видимых колёс — SDK ничего не позирует; см. позирование моделей колёс.

Пространство и ориентация

Пространство шасси — вперёд −Z, вверх +Y — собственная конвенция SDK, та, что сообщает node.forward. Если модель смотрит в другую сторону, поверни модель (дочерний узел), никогда не узел шасси.

Позиция узла колеса — где его центр покоится на ровной земле. Ставь её ниже начала координат шасси: колесо, заданное выше, висит в воздухе, и машина тогда крутит мотор впустую. SDK никогда не двигает сам узел колеса — где колесо находится сейчас, это показание.

Физика владеет трансформом шасси. Управляй им через входы, а не покадровыми записями position. Запись position действительно ставит машину (она доходит до тела шасси, как у любого физического узла), но сохраняет скорость машины и её крен — чтобы вернуть машину на дорогу, используй reset(p).

Входы — педали

TypeScript
car.vehicle.throttle = 1      // 0..1 — педаль двигателя; без знака
car.vehicle.ratio = 2.66      // ЕДИНСТВЕННОЕ передаточное число, на котором работает двигатель (по умолчанию 1): отрицательное = задний ход, 0 = нейтраль
car.vehicle.clutch = 1        // 0..1 включение (по умолчанию 1); 0 = двигатель отсоединён от колёс
car.vehicle.steer = -0.5      // −1..1, вправо — положительное
car.vehicle.steerForce = 1    // 0..1 — как крепко руки держат руль с `feedback` (по умолчанию 1)
wheel.brake = 1500            // Н·м тормозного момента на ЭТОМ колесе (по умолчанию 0)

Пиши их из updateBefore системы: машина передаёт их движку в том же раннем проходе (она тикает после порядка по умолчанию), так что шаг физики этого кадра едет на них. Они липкие — значение продолжает действовать, пока ты не запишешь его снова, — а ненулевые throttle, steer или brake держат машину бодрствующей (простаивающая машина без входа засыпает). throttle, steer, steerForce и clutch зажимаются в свои диапазоны; отрицательный brake считается 0.

  • ratio — выбор всей трансмиссии: обороты двигателя = обороты колеса × ratio × 3.42 (главная передача). Его знак — направление, так что задний ход — просто отрицательная передача.
  • steer без руля с обратной связью — угол дорожных колёс как доля steering.maxAngle, применяемый как есть — сужение со скоростью или ограничение скорости поворота (клавиатура просит полный выворот за один кадр) — дело игры. С steering.feedback это то, куда руки водителя направляют руль.
  • Тормоз — на колесо: ножной тормоз — момент на каждом колесе, ручник — задняя пара, ABS пульсирует ими. Тормозящее колесо зажимается на физическом пределе шины.

Коробка передач — дело игры

Выбор передачи — политика, а не физика: несколько строк над rpm и ratio. Пятиступенчатый автомат:

TypeScript
const RATIOS = [2.66, 1.78, 1.3, 1.0, 0.74]
const REDLINE = 6000                                     // = engine.maxRpm
class Automatic extends System<'automatic'> {
  static after = [Driver]                                // переопределяет переднюю передачу Driver
  gear = 0                                               // 0 нейтраль, 1… вперёд
  private wait = 0
  updateBefore(dt: number) {
    const v = car.vehicle
    this.wait = Math.max(0, this.wait - dt)
    if (this.gear === 0 && v.throttle > 0) this.gear = 1
    if (this.gear > 0 && this.wait === 0) {
      if (v.rpm > REDLINE * 0.67 && this.gear < RATIOS.length) { this.gear++; this.wait = 0.5 }
      else if (v.rpm < REDLINE * 0.33 && this.gear > 1) { this.gear--; this.wait = 0.5 }
    }
    if (v.ratio > 0) v.ratio = this.gear === 0 ? 0 : RATIOS[this.gear - 1]
  }
}
scene.system(Automatic)

Секвентальная механика — клавиша, переключающая передачу; огибающая сцепления во время переключения — clutch, плавно проведённый от 0 обратно к 1. Ни того ни другого в SDK нет намеренно.

Vehicle — машина целиком

TypeScript
node.aspect(Vehicle, {
  mass?: number                      // кг, по умолчанию 1200. Колёса в этой модели безмассовые
  centerOfMass?: number              // высота в пространстве шасси, м; по умолчанию: середина формы по высоте
  driveAxle?: 'rear' | 'front' | 'all' | 'none'   // по умолчанию 'rear'; 'none' = прицеп, толкаемая тележка
  differential?: 'lsd' | 'open' | 'locked' | number   // по умолчанию 'lsd'
  antiRoll?: boolean | number        // по умолчанию true (0.01 от пружины колеса)
  maxTilt?: number                   // градусы, после которых ограничение выправляет машину; по умолчанию 60 (180 = выкл)
  friction?: number                  // трение тела ШАССИ — не шин; по умолчанию 0.6
  clutchStrength?: number            // по умолчанию 10
  clutchCapacity?: number            // Н·м, после которых сцепление проскальзывает; по умолчанию 0 = без предела

  engine?:   { torque?, maxRpm?, idleRpm?, curve?, inertia?, braking? }
  steering?: { maxAngle?, caster?, trail?, trailAtPeak?, feedback?, driver? }
  aero?:     { downforce?, drag? }
})

engine — включая его характер

TypeScript
engine: { torque: 600, maxRpm: 7000, curve: [{ x: 0, y: 0.8 }, { x: 0.66, y: 1 }, { x: 1, y: 0.8 }] }

torque (Н·м, по умолчанию 500) — самая главная ручка «насколько она быстрая», а maxRpm (по умолчанию 6000) — отсечка, собственный зажим двигателя; заикание ограничителя оборотов — дело игры. curve — то, что делает двигатели разными на ощупь: точки { x, y } в нормализованных единицах — x — доля отсечки, y — доля torque, x по возрастанию. Пример выше — обычный бензиновый двигатель, с пиком на двух третях и спадом к отсечке; [{ x: 0, y: 1 }, { x: 1, y: 1 }] — плоская, так ощущается электромотор. Без неё берётся собственная широкая кривая Jolt.

inertia (кг·м², по умолчанию 0.5) — как охотно он набирает обороты с выжатым сцеплением, braking (по умолчанию 0.2) — торможение двигателем, а idleRpm (по умолчанию 1000) — пол, ниже которого он не опускается. idleRpm: 0 убирает пол: двигатель тогда можно затянуть до остановки — заглохнуть, — а удержание холостых (регулятор над throttle) или запуск стартером — дело игры.

Сцепление — clutchStrength, clutchCapacity

clutchStrength (по умолчанию 10) — как жёстко полностью включённое сцепление связывает двигатель и колёса; вход clutch масштабирует его 0..1. Малые значения проскальзывают, большие — сваривают намертво. При clutchCapacity 0 у сцепления нет потолка момента. Задай его (у дорожной машины ~1.5× пикового момента двигателя) — и сцепление проскальзывает на этих Н·м × clutch — сухое сцепление: старт можно дозировать, а сцепление, брошенное на слишком высокой передаче, тянет двигатель вниз вместо того, чтобы дёрнуть машину. При idleRpm: 0 это затягивание — глохнущий мотор.

differential — и кнопка дрифта

lsd — ограниченное проскальзывание (коэффициент Jolt 1.4 — разумное умолчание). open отдаёт вывешенному или буксующему колесу весь момент. locked заставляет оба колеса оси крутиться с одной скоростью, так что пара срывается вместе, а не по одному — это и есть дрифтовая настройка, и переключиться на неё на ходу — одна строка (см. перенастройку). Число — сырой коэффициент max/min скоростей колёс (> 1; меньше — сильнее блокировка).

aero — почему машина должна поворачивать ЛУЧШЕ, чем быстрее едет

Оба числа задаются одинаково — как доля собственного веса машины на 30 м/с (108 км/ч) — и оба растут с квадратом скорости от этой точки, так что 0.5 на 30 м/с — это 2.0 на 60 м/с. Одна мысленная модель для обоих и никаких коэффициентов сопротивления или лобовых площадей, которые надо выдумывать.

Сцепление шин — константа, а сила, которую требует поворот, растёт с v². Без прижимной силы (по умолчанию 0) любая машина поэтому на скорости вылетает с дороги — именно это и значит «она не поворачивает на 100 км/ч». Прижимная сила тоже растёт с v², так что они идут вровень: 0.6 примерно удваивает способность машины поворачивать на 150 км/ч. Дорожная машина ближе к 0.1, гоночная — далеко за 1. Она давит вдоль собственной оси «вниз» шасси (это и держит машину прижатой на гребне) и только пока колесо на земле — машину на крыше её собственное антикрыло никогда не запустит.

drag (по умолчанию 0.03) — то, что вообще даёт машине максимальную скорость.

antiRoll — насколько кренится кузов

true (по умолчанию) — стабилизатор жёсткостью 0.01 от собственной пружины колеса; число — этот коэффициент напрямую, 0 / false убирает стабилизаторы. Коэффициент, а не Н/м, так что он значит то же самое, когда ты меняешь mass или suspension.stiffness колеса.

Эти числа куда меньше, чем выглядят, и прирост крутой. Стабилизатор Jolt — не пружина параллельно подвеске: он подаёт lengthDifference × stiffness × dt как смещение в собственное мягкое ограничение подвески, так что коэффициент, читающийся как «1% пружины», уже значим. Измерено на машине 1250 кг, тот же поворот:

коэффициент крен при 0.86 g крен при ~1.5 g
0 1.3° 2.5°
0.01 (true) 0.6° —
0.04 — 0.4°
0.15 — 0.2° — кузов остаётся плоским
0.5 машина ПРЫГАЕТ: стабилизатор отключается при отрыве колеса и работает как реле

Так что это ручка того, насколько машина заметно кренится. Это не ручка отрыва колеса: стабилизаторы не меняют, сколько нагрузки переходит между сторонами, только насколько кузов наклоняется, пока это происходит. Для отрыва понижай centerOfMass.

steering — выворот, тяга шин и руль с телом

TypeScript
steering: { maxAngle: 35 }                                    // по умолчанию: steer = ±1 — это ±35°
steering: { maxAngle: 35, feedback: true }                    // руль, на который давят в ответ шины
steering: { feedback: { powerSteering: 0.3 }, driver: { maxTorque: 90 } }

maxAngle (градусы, по умолчанию 35) — что значит steer = ±1. Геометрия передней оси — caster (механическое плечо, м, по умолчанию 0.03), trail (пневматическое плечо шины, м, по умолчанию 0.025) и trailAtPeak (что от него остаётся на пиковом угле увода шины, по умолчанию 0.5) — рычаг стабилизирующего момента шин, который сообщает steerTorque (Н·м, положительный тянет руль вправо): тяжёлый на скорости, лёгкий, когда перед срывается. Он есть с рулём обратной связи и без — сигнал, которым управляется руль с force feedback.

feedback делает руль телом, которое движок интегрирует каждый подшаг. Тогда steer — куда его направляют руки водителя, а steerForce — как крепко они его держат; где окажутся дорожные колёса — физика: на скорости зажатая клавиша оседает, не доходя до упора, — там, где момент рук уравновешивает тягу шин; отпусти (steerForce = 0) — и руль сам центрируется в повороте на сцеплении или уходит в контрруление в заносе. feedback: true берёт умолчания; объект настраивает колонку:

feedback по умолчанию
inertia 1 кг·м² — руль + колонка + рейка
damping 25 Н·м·с/рад — не даёт отпущенному рулю звенеть вокруг центра
friction 8 Н·м — собственное сухое трение колонки
parkingFriction 40 Н·м на (почти) стоящей машине, сходит на нет к 1.5 м/с — почему стоящую машину тяжело рулить
powerSteering 0.5 0..1 — доля стабилизирующего момента, которую руль никогда не видит (1 = мёртвый обод)
endStop { band: 16, torque: 120, damping: 150 } резина перед упором: градусы, Н·м на упоре, Н·м·с/рад × глубина

driver — руки на руле с обратной связью: maxTorque (Н·м, по умолчанию 130 — потолок того, сколько выворота зажатая клавиша удерживает против шин), stiffness (Н·м/рад, по умолчанию 500), maxRate (град/с, с которой руки могут поворачивать дорожные колёса, по умолчанию 120) и rampTime (с, за которые нарастает момент рук, по умолчанию 0.1). Что руки делают — направляют, держат, отпускают — дело игры, покадрово, через steer и steerForce.

Wheel — одно колесо

Аспект на собственном узле колеса. Его позиция — положение колеса в покое; всё остальное об этом колесе живёт здесь, где его хранит и Jolt.

TypeScript
hub.aspect(Wheel, {
  radius?: number                    // м; по умолчанию: измеряется по отрисованным границам узла, иначе 0.35
  width?: number                     // м; по умолчанию: измеряется, иначе 0.25
  steer?: boolean                    // по умолчанию: передняя ось
  driven?: number                    // ДОЛЯ момента двигателя на этом колесе; по умолчанию 1 на driveAxle, иначе 0
  axle?: number                      // группа оси, спереди назад; по умолчанию: группируется по Z
  suspension?: { travel?, stiffness?, damping? }   // м, Гц, коэффициент; по умолчанию 0.4, 1.4, 0.5
  tire?: Tire                        // по умолчанию Tire.road()
})

hub.wheel.brake = 4500               // вход: Н·м в этом кадре (см. Входы)

driven — доля, а не флаг: доля оси — сумма её двух колёс, деление лево/право — их отношение — 0.6 / 0.4 перед-зад — это driven: 0.3 на каждом переднем колесе и 0.2 на каждом заднем. axle объединяет колёса в пары для дифференциала и стабилизатора; задавай его, когда колёса не стоят аккуратными парами лево/право (колёса в пределах 0.35 м друг от друга по Z по умолчанию делят ось).

tire — кривые как точки

TypeScript
new Node().aspect(Wheel, { tire: Tire.drift() })             // шина, которую можно держать боком
for (const w of car.vehicle.wheels) w.node.aspect(Wheel, { tire: Tire.scale(Tire.road(), 0.7) })   // мокрая дорога, на ходу
Note

меняй шину живой машины, переконфигурируя аспект колеса, как выше — машина перенастраивается перед следующим шагом. Прямое присваивание wheel.tire не доходит до движка, пока что-то другое не перенастроит машину.

Tire — числа до самого низа, как и кривая момента двигателя:

TypeScript
interface Tire {
  side: Point[]       // боковое трение по УГЛУ увода: { x: градусы, y: трение }, x по возрастанию
  forward: Point[]    // продольное трение по КОЭФФИЦИЕНТУ проскальзывания: { x: коэффициент, y: трение }
  traction?: number   // зажим ПРИВОДНОГО импульса как кратное трение × нагрузка (1 = физическая шина)
  circle?: number     // 0..1 круг трения: сколько бокового сцепления теряет буксующее / заблокированное колесо
}

Tire.road()      // штатная шина Jolt: боковой пик 1.2 на 3°, 1.0 от 20°; traction 10, circle 0 — приклеена
Tire.arcade()    // пик держится до конца по обеим осям: машина не срывается
Tire.drift()     // боковое сцепление падает до 0.7 после 25°, traction 2, circle 0.85 — избыточная поворачиваемость под газом
Tire.scale(t, k) // обе кривые × k — ползунок сцепления, холодная шина; новая Tire, t не тронута
resolveTire(t)   // что получает движок: пустые кривые и отсутствующий traction берут значения road, circle зажат в 0..1

Пресеты — свежие списки при каждом вызове — скопируй один и правь. Значение traction у road, 10 (собственное значение из примера Jolt), означает, что ведущее колесо практически никогда не срывается в пробуксовку; всему, во что должен вцепляться круг трения, нужен traction около 1–2. То, что колесо получает на деле, объединяется с землёй, на которой оно стоит, как sqrt(tire × groundFriction), а трение тела по умолчанию 0.6 — так что дороге, по которой машина должна по-настоящему поворачивать, нужен Physics { friction: 1 }.

suspension — один travel не даёт колесу больше хода

Положение колеса в покое — там, где пружина несёт вес машины, и этот прогиб равен g / (2πf)² — он зависит только от stiffness, не от travel. Прогиб — это ровно и то, насколько колесо ещё может отвиснуть. Так что повышение одного travel покупает только запас на сжатие; чтобы колёса доставали дальше вниз — в ямы, с гребней, при приземлении, — смягчи и пружину. При умолчаниях (travel: 0.4, stiffness: 1.4, damping: 0.5) это 12.7 см отвисания и 11.3 см сжатия.

Позирование моделей колёс

SDK ничего не позирует: position колеса (пространство шасси, вдоль хода подвески), steerAngle и spin — то, из чего строится модель колеса — в поздней фазе, после шага. Клади каждую модель на узел, который — сосед своей ступицы под шасси:

TypeScript
const DEG2RAD = Math.PI / 180
const tire = Material.lit({ color: '#20242e' })
const art = new Map<Wheel, Node>()
for (const w of car.vehicle.wheels) {
  const disc = Mesh.cylinder({ material: tire, radius: R })
  disc.scale = [1, 0.25, 1]
  disc.eulerAngles = [0, 0, 90]                          // ось цилиндра — +Y; ось колеса — +X
  const holder = new Node()
  holder.add(disc)
  chassis.add(holder)
  art.set(w, holder)
}
class WheelArt extends System<'wheelArt'> {
  private angle = new Map<Wheel, number>()
  update(dt: number) {                                   // ПОЗДНО: устоявшиеся показания
    for (const w of car.vehicle.wheels) {
      const a = ((this.angle.get(w) ?? 0) + w.spin * dt) % (2 * Math.PI)
      this.angle.set(w, a)
      const n = art.get(w)!
      n.position = w.position
      n.quaternion = Quat.fromAxisAngle([0, 1, 0], -w.steerAngle * DEG2RAD).mul(Quat.fromAxisAngle([1, 0, 0], -a))
    }
  }
}
scene.system(WheelArt)

Оба минуса — это движок, сообщающий в терминах водителя — руль положителен вправо, вращение положительно вперёд — в правосторонней сцене, где вперёд −Z. Тормозной суппорт — то же самое без вращения.

reset и импульсы

TypeScript
car.vehicle.reset(position?, rotation?): this   // телепорт в вертикальное положение, сброс всего движения
car.vehicle.applyImpulse(v): this               // кг·м/с на шасси — площадка ускорения, взрыв

reset() без аргументов переворачивает машину обратно там, где она лежит (сохраняет курс, сбрасывает крен и тангаж). Это единственное, что обнуляет скорости, обороты двигателя и вращение колёс — простая запись position этого не делает. Входы сохраняются и отправляются снова в следующем раннем проходе.

Перенастройка живой машины

Переконфигурирование прикреплённого аспекта перенастраивает его на месте — так работает кнопка дрифта:

TypeScript
car.aspect(Vehicle, { differential: drift ? 'locked' : 'lsd' })

Живое: differential, engine, steering, aero, maxTilt, friction, clutchStrength, clutchCapacity, antiRoll (его жёсткость, на осях, у которых есть стабилизатор) и tire каждого колеса (переконфигурируй это колесо: w.node.aspect(Wheel, { tire })).

Структурное — запечено в ограничение при прикреплении: mass, centerOfMass, какие колёса существуют и где, их suspension, доли driven. Изменение одного из них означает повторное прикрепление аспекта; новая mass на живой машине предупреждает и оставляет построенную.

Чтение машины

TypeScript
car.vehicle.speed           // м/с вдоль передней оси машины, отрицательная на заднем ходу (× 3.6 для км/ч)
car.vehicle.rpm
car.vehicle.wheelsOnGround / .grounded
car.vehicle.velocity        // Vec3, мир, м/с
car.vehicle.angularVelocity // Vec3, мир, РАДИАНЫ/с
car.vehicle.yawRate         // град/с вокруг вертикали, положительная при повороте ВПРАВО (тот же знак, что у steer)
car.vehicle.slipAngle       // град между носом и движением кузова, + при движении правее него
car.vehicle.driftAngle      // град ЗАНОСА: движение задней оси против оси трансмиссии
car.vehicle.steerAngle      // град, где на самом деле стоят рулевые колёса, вправо — положительное
car.vehicle.steerTorque     // Н·м, стабилизирующий момент шин, + тянет вправо
car.vehicle.wheels          // аспекты Wheel, передняя ось первой
car.vehicle.id / .bodyId    // нативные id машины / тела шасси (0 = не прикреплена)

Входы тоже читаются обратно (throttle, steer, ratio, …). driftAngle равен 0 в чистом повороте на любой скорости — в отличие от slipAngle, у которого кузов на шаге пешехода и полном вывороте едет на ~20° в сторону от носа, — читается за 90° у машины, развёрнутой на передней передаче, и нарастает на первых 1–2 м/с: это число, которое нужно гиро-ассисту или дрифт-метру.

На колесо, на самом колесе:

contact касается земли в этом шаге
slip продольный коэффициент проскальзывания, со знаком: 0 катится синхронно, к +1 буксует, к −1 блокируется
slipAngle градусы, со знаком: пятно, скользящее вправо от шины, — положительное. После ~3° она срывается
steerAngle градусы, положительные вправо
spin рад/с, положительное вперёд (× radius ≈ скорость, с которой шина прокладывает след)
load Н — сила подвески на колесо (0 в воздухе)
sideForce, forwardForce Н — силы шины в этом шаге, + вправо от неё / + толкая машину вперёд
sideSlipSpeed, slipSpeed м/с — как быстро пятно скользит вбок / протектор бежит по дороге
compression 0 полностью разжата … 1 полностью сжата
position Vec3, пространство шасси — где центр колеса сейчас
TypeScript
// куда идут дым дрифта или следы шин: центры скользящих колёс, в мировом пространстве
const marks: Vec3[] = []
for (const w of car.vehicle.wheels) {
  if (w.contact && Math.abs(w.slipAngle) > 8) marks.push(car.worldMatrix.transformPoint(w.position))
}

Гейтинг хоста

Всё здесь требует 3D-физики — защищай через Physics.supported. Без неё аспект прикрепляется, id остаётся 0 и каждый вызов — no-op. Headless-рендерер отказывается от машины (она никогда не движется): тестируй машину через lecodes render --sim или --desktop (physics.md).

Не в v1

Мотоциклы и гусеничная техника (у Jolt есть оба контроллера), а также трансмиссия вообще без двигателя (аркадная модель «газ → сила»).

Смотрите также

  • Physics — Shape, friction поверхности, события контактов
  • Systems — где живут водитель, коробка передач и графика колёс
  • Input — клавиши, оси геймпада для газа и руля