M1: constraint chassis (springs, servo steering, motor wheels)
CI / quality (push) Successful in 54s

- New shared Chassis.luau: geometry constants + per-car tuning from stats
- CarManager.buildCarModel rebuilt: prismatic+spring suspension per corner,
  servo-hinge steering on front knuckles, motor-hinge driven wheels,
  hidden dense ballast for anti-roll; VehicleSeat kept as input device only
- New client ChassisController: drives motors/servos from seat
  ThrottleFloat/SteerFloat per Heartbeat; speed-sensitive steering lock;
  engine braking on throttle release
- Driver gets network ownership of every unanchored part (constraint
  assemblies are separate; missing one causes stuttery physics)
- Docs: MESH_SWAP.md rewritten for shell-on-chassis workflow; ROADMAP updated
This commit is contained in:
2026-07-22 00:20:59 -04:00
parent 07a67204ca
commit b3a1044df6
6 changed files with 482 additions and 97 deletions
+50 -44
View File
@@ -1,11 +1,13 @@
# Swapping the placeholder RX-7 for the real mesh (M1)
Right now every car is a coloured block + a `VehicleSeat` so the game is drivable
today. This is the **one file** you touch to make the RX-7 a real model:
`src/server/CarManager.luau` → the `buildCarModel` function.
The car is now a **constraint chassis** (springs + steering servos + motor-driven
wheels — see `src/shared/Chassis.luau` and `CarManager.buildCarModel`). The
placeholder Body/Cabin blocks are just the visual shell riding on that chassis.
Making the RX-7 real means **replacing the shell, not the chassis**: weld a
MeshPart body onto the `Body` part and hide the placeholder blocks.
Nothing else (saving, the allowlist, car switching, the map, anti-cheat) needs to
change. That's the whole point of isolating model-building in one function.
Nothing else (saving, the allowlist, car switching, driving feel, the map,
anti-cheat) needs to change.
## Reference photos
The real car's reference photos live on the NAS (too large to commit to git):
@@ -17,7 +19,7 @@ unraid-1:/mnt/user/data/yy-roblox-games/0-fast-n-furious-sim/001-1993 Mazda RX-7
A single downscaled reference is included at `assets/reference/rx7-reference.png`
for quick lookups.
## Step 1 — Get a model into Roblox
## Step 1 — Get a mesh into Roblox
Options, easiest first:
1. **Buy or find a free RX-7 FD model** on the Roblox Creator Marketplace/toolbox,
or
@@ -25,58 +27,62 @@ Options, easiest first:
(`.fbx`/`.obj`) via Studio's 3D importer, or
3. Commission one.
You want a single `Model` whose parts are welded together, with a clear "body"
part you can drive. Keep the part count low (mobile perf).
You want the car **body shell as a single MeshPart** (or a small welded Model
with a clear root). Wheels can be separate meshes (see Step 4). Keep the part
count low (mobile perf). This step needs Studio — mesh assets upload through
Roblox's asset pipeline.
## Step 2 — Store it where Rojo/Studio can clone it
Put the finished `Model` under `ReplicatedStorage` (e.g. a `CarModels` folder).
Two ways to manage it:
## Step 2 — Store it where the server can clone it
Put the shell under `ReplicatedStorage/CarModels/<carId>` (e.g.
`CarModels/rx7_fc`). Two ways to manage it:
- **Simple:** keep it in the Studio place file as a real Instance (not synced by
Rojo). Fine for a single hero car.
- **Version-controlled:** export the model to a `.rbxm` and reference it from the
Rojo project so it lives in git. See Rojo's `$path` docs for `.rbxm` model files.
## Step 3 — Rewrite `buildCarModel` to clone the real mesh
Replace the block-building body of `buildCarModel(car, maxSpeed, torque, turnSpeed)`
with something like:
## Step 3 — Attach the shell in `buildCarModel`
In `CarManager.buildCarModel`, after the Body part is created:
```lua
local CAR_MODELS = ReplicatedStorage:WaitForChild("CarModels")
local CAR_MODELS = ReplicatedStorage:FindFirstChild("CarModels")
local function buildCarModel(car, maxSpeed, torque, turnSpeed)
-- Look up a real model by car id; fall back to the block builder if missing.
local template = CAR_MODELS:FindFirstChild(car.Id)
if not template then
return buildBlockCar(car, maxSpeed, torque, turnSpeed) -- keep the old code as a fallback
-- inside buildCarModel, after `body` exists:
local shellTemplate = CAR_MODELS and CAR_MODELS:FindFirstChild(car.Id)
if shellTemplate then
local shell = shellTemplate:Clone()
-- Position the shell over the body, then weld it on.
shell:PivotTo(body.CFrame * CFrame.new(0, SHELL_Y_OFFSET, 0))
for _, p in ipairs(shell:GetDescendants()) do
if p:IsA("BasePart") then
p.CanCollide = false -- the Body part remains the collider
p.Anchored = false
end
end
local model = template:Clone()
-- Ensure it has a VehicleSeat named "DriveSeat" welded to the driving body.
local seat = model:FindFirstChild("DriveSeat")
if not seat then
seat = Instance.new("VehicleSeat")
seat.Name = "DriveSeat"
-- weld seat to model.PrimaryPart, position it in the cabin...
seat.Parent = model
end
seat.MaxSpeed = maxSpeed
seat.Torque = torque
seat.TurnSpeed = turnSpeed
assert(model.PrimaryPart, "Car model needs a PrimaryPart set")
return model
local weld = Instance.new("WeldConstraint")
weld.Part0 = body
weld.Part1 = shell.PrimaryPart or shell:FindFirstChildWhichIsA("BasePart")
weld.Parent = body
shell.Parent = model
-- Hide the placeholder blocks:
body.Transparency = 1
local cabin = model:FindFirstChild("Cabin")
if cabin then cabin:Destroy() end
end
```
Keep the current block-builder renamed as `buildBlockCar` so any car without a
real mesh still works. That way you can import cars one at a time.
Cars without a mesh keep the block look automatically — import one car at a time.
## Step 4 (recommended, M1) — Move to a real chassis
The `VehicleSeat` is great for "it drives today" but for real suspension/grip,
adopt a proven **constraint chassis** (A-Chassis or a modern equivalent). At that
point `buildCarModel` clones the chassis rig and attaches the RX-7 body to it.
Still one function; still no changes elsewhere.
## Step 4 (polish) — Real wheel meshes
The chassis wheels are plain cylinders (`Wheel_FL` … `Wheel_RR`). To make them
look real, weld a wheel MeshPart (CanCollide=false) onto each cylinder and set
the cylinder's `Transparency = 1`. The cylinder stays the physics tyre — mesh
collision on wheels is unreliable and slow.
## Sizing gotcha
The chassis geometry (`Chassis.BODY_SIZE`, `WHEEL_RADIUS`, `TRACK_HALF`,
`WHEELBASE_HALF`) is tuned for a ~6×12 stud footprint. Scale the imported mesh
to fit the chassis — don't reshape the chassis around the mesh, or the driving
feel changes for every car.
## Verify
- `rojo build --output build.rbxl` compiles.
+8 -6
View File
@@ -14,12 +14,14 @@ Spawn on the neon expressway, drive a car, switch cars, respawn. RX-7 demo-locke
to owner + son. Cars persist between sessions. CI green on a self-hosted runner.
## M1 — Make driving feel GOOD (the real work, do this next)
- [ ] Import the **real RX-7 mesh**; swap `CarManager.buildCarModel` only.
See `docs/MESH_SWAP.md` for the exact steps.
- [ ] Adopt a proven **constraint chassis** (A-Chassis or a modern equivalent)
for real suspension/grip instead of the raw VehicleSeat. Do NOT hand-roll
physics.
- [ ] Tune acceleration curve, grip, and the chase camera.
- [x] Adopt a **constraint chassis** for real suspension/grip instead of the raw
VehicleSeat: springs + prismatic suspension, servo steering (speed-sensitive
lock), motor-driven wheels, engine braking, ballast anti-roll. Shared tuning
in `src/shared/Chassis.luau`; client-side `ChassisController` drives it
(driver owns network physics). **Needs a Studio playtest to tune feel.**
- [ ] Import the **real RX-7 mesh**; weld the shell onto the chassis `Body`.
See `docs/MESH_SWAP.md` for the exact steps (needs Studio for asset upload).
- [ ] Tune acceleration curve, grip, and the chase camera (after first playtest).
- [ ] Sound: engine loop, tire screech, boost.
**Exit test:** the RX-7 is genuinely fun to just drive around. Ship nothing else
until this is true.