Fix monster-truck stance and head-through-roof clipping

Wheels: WHEEL_RADIUS was 2.0 (4.0-stud tyre on a 19-stud car = 21% of
length; real cars are ~14%). Derived from SHELL_LENGTH instead -> 2.70
dia, ratio 0.142. Suspension drop 2.2 -> 0.85 and GROUND_OFFSET
correspondingly lowered, dropping the body onto the wheels.

Roof fit, three separate bugs:
1. HEAD_ABOVE_SEAT (3.27) was the head CENTRE, not the crown. A default
   R15 head is 1.20 tall so the crown is 3.87 up -- the constant was
   short by half a head, which is exactly what poked through.
2. The adaptive seat solve measured the roof from the shell's bounding
   box top, which includes spoilers/antennas/stray parts (up to +0.74
   of junk). Now uses the largest part's top -- the real roofline.
3. wanted/seatLocal is a pan height but HEAD_ABOVE_SEAT is measured from
   the seat CENTRE, so every car silently lost Size.Y/2 of clearance
   (hyper_x got 0.07 instead of the requested 0.30).

Also lowered SHELL_DROP 0.6 -> 0.25 and let the seat clamp sink to
GROUND_OFFSET - SHELL_DROP, since hyper_x has the lowest roof (4.64)
and had no headroom to give.

Verified live in Play, avatar seated, all five models INSIDE the roof:
hyper_x 0.33, rx7_fc 0.31, silv_s15 0.37, gtr_r34 0.44, hatch_junk 1.09
This commit is contained in:
2026-08-01 02:08:31 -04:00
parent 37a83e0e8f
commit 85730f0038
2 changed files with 223 additions and 14 deletions
+173 -9
View File
@@ -51,7 +51,10 @@ local SETTLE_TIME = 1.0
-- How far to sink an imported mesh shell into the chassis so it doesn't look
-- like it's hovering. Purely cosmetic (shell parts are Massless/CanCollide=false).
local SHELL_DROP = 0.6
-- Sink the shell slightly so the tyres aren't fully swallowed by the bodywork.
-- Every stud of drop is a stud of cabin height lost, and hyper_x (lowest roof
-- in the library at 4.64) has none to spare, so keep this small.
local SHELL_DROP = 0.25
-- Collision groups ------------------------------------------------------------
-- The wheels deliberately overlap the chassis: with BODY_SIZE.X = 6 the body
@@ -63,6 +66,13 @@ local SHELL_DROP = 0.6
-- group that ignores the chassis is the fix; the geometry is intentional.
local WHEEL_GROUP = "CarWheel"
local CHASSIS_GROUP = "CarChassis"
-- Seating the driver on the cabin FLOOR (see Chassis.SEAT_ABOVE_GROUND) tucks
-- the avatar down inside the body slab, so its legs/torso now genuinely
-- interpenetrate the chassis. Overlapping colliders is exactly what launches a
-- character across the map, so the driver gets its own group that ignores the
-- car. The driver still collides with the world, so it can't clip through
-- terrain when ejected.
local DRIVER_GROUP = "CarDriver"
local groupsReady = false
local function ensureCollisionGroups()
@@ -73,6 +83,7 @@ local function ensureCollisionGroups()
local ok = pcall(function()
PhysicsService:RegisterCollisionGroup(WHEEL_GROUP)
PhysicsService:RegisterCollisionGroup(CHASSIS_GROUP)
PhysicsService:RegisterCollisionGroup(DRIVER_GROUP)
end)
if not ok then
-- Already registered (Play-solo re-entry) -- harmless.
@@ -80,6 +91,23 @@ local function ensureCollisionGroups()
PhysicsService:CollisionGroupSetCollidable(WHEEL_GROUP, CHASSIS_GROUP, false)
PhysicsService:CollisionGroupSetCollidable(CHASSIS_GROUP, CHASSIS_GROUP, true)
PhysicsService:CollisionGroupSetCollidable(WHEEL_GROUP, WHEEL_GROUP, false)
PhysicsService:CollisionGroupSetCollidable(DRIVER_GROUP, CHASSIS_GROUP, false)
PhysicsService:CollisionGroupSetCollidable(DRIVER_GROUP, WHEEL_GROUP, false)
end
--- Put every part of a character into the driver collision group (or back into
--- Default when they get out), so a low-seated avatar can't fight the chassis.
local function setCharacterGroup(character: Model?, group: string)
if not character then
return
end
for _, d in ipairs(character:GetDescendants()) do
if d:IsA("BasePart") then
pcall(function()
d.CollisionGroup = group
end)
end
end
end
-- Helpers ---------------------------------------------------------------------
@@ -260,10 +288,14 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
seat.HeadsUpDisplay = false
seat.CanCollide = false
seat.Color = Color3.new(0.1, 0.1, 0.1)
-- Seat pan just above the body slab (top = H/2): low enough to look tucked
-- in the cabin, high enough that the avatar's legs never interpenetrate
-- the slab (which causes physics launches).
seat.CFrame = origin * CFrame.new(0, Chassis.BODY_SIZE.Y / 2 + 0.3, 0.2)
-- Seat pan on the CABIN FLOOR, measured up from the ground plane the wheels
-- rest on -- NOT up from the body slab. The slab sits at the car's vertical
-- middle, so the old "slab top + 0.3" placement put the driver's hips at the
-- car's waistline and its head through the roof. Sitting the avatar this low
-- means its torso now overlaps the slab, which is why the driver goes in its
-- own collision group (see DRIVER_GROUP).
seat.CFrame = origin
* CFrame.new(0, Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND + seat.Size.Y / 2, 0.2)
seat.Parent = model
local seatWeld = Instance.new("WeldConstraint")
seatWeld.Part0 = body
@@ -288,10 +320,128 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
if shellTemplate then
local shell = shellTemplate:Clone()
shell.Name = "Shell"
-- Seat the shell on the chassis: drop it so the mesh's underside sits at
-- roughly the body plane rather than floating at the body's centreline.
local ext = shell:GetExtentsSize()
shell:PivotTo(body.CFrame * CFrame.new(0, ext.Y / 2 - Chassis.BODY_SIZE.Y / 2 - SHELL_DROP, 0))
-- Measure the shell from its PARTS, not from Model:GetExtentsSize() /
-- Model:GetBoundingBox(). Both of those return an axis-aligned box that
-- is inflated by rotated parts: silv_s15 reports H=12.32 when its real
-- geometry is only 4.71 tall, which made it hover ~4 studs in the air.
-- Accumulating each part's own rotated half-extents gives the true box.
local function trueAABB(model)
local lo = Vector3.new(math.huge, math.huge, math.huge)
local hi = Vector3.new(-math.huge, -math.huge, -math.huge)
local found = false
for _, p in ipairs(model:GetDescendants()) do
if p:IsA("BasePart") then
found = true
local cf, s = p.CFrame, p.Size
local hx = 0.5 * (math.abs(cf.RightVector.X * s.X)
+ math.abs(cf.UpVector.X * s.Y) + math.abs(cf.LookVector.X * s.Z))
local hy = 0.5 * (math.abs(cf.RightVector.Y * s.X)
+ math.abs(cf.UpVector.Y * s.Y) + math.abs(cf.LookVector.Y * s.Z))
local hz = 0.5 * (math.abs(cf.RightVector.Z * s.X)
+ math.abs(cf.UpVector.Z * s.Y) + math.abs(cf.LookVector.Z * s.Z))
local c = p.Position
lo = Vector3.new(math.min(lo.X, c.X - hx), math.min(lo.Y, c.Y - hy), math.min(lo.Z, c.Z - hz))
hi = Vector3.new(math.max(hi.X, c.X + hx), math.max(hi.Y, c.Y + hy), math.max(hi.Z, c.Z + hz))
end
end
if not found then
return nil, nil, nil
end
return lo, hi, hi - lo
end
-- 1. NORMALISE SIZE. Library meshes range 15.6 -> 19.0 studs long at
-- native scale, so cars looked inconsistent and the short ones had no
-- cabin room for the avatar. Uniform Model:ScaleTo keeps each mesh's
-- own proportions while making every car the same length.
-- The long axis is NOT always Z: gtr_r34 and hyper_x are modelled
-- rotated 90 degrees (length on X), so scaling by .Z blew them up to
-- ~2x. Use whichever horizontal axis is actually longer.
local _, _, rawSize = trueAABB(shell)
if rawSize then
local rawLen = math.max(rawSize.X, rawSize.Z)
if rawLen > 0.01 then
shell:ScaleTo(Chassis.SHELL_LENGTH / rawLen)
end
end
-- 2. SIT IT ON THE GROUND. Align the true box's BOTTOM to the ground
-- plane the wheels rest on, then sink by SHELL_DROP so tyres aren't
-- swallowed. Measured against the pivot, since PivotTo moves the pivot.
local lo2 = trueAABB(shell)
local bottomOffset = 0
if lo2 then
bottomOffset = lo2.Y - shell:GetPivot().Position.Y
end
shell:PivotTo(
body.CFrame * CFrame.new(0, Chassis.GROUND_OFFSET - bottomOffset - SHELL_DROP, 0)
)
-- 3. DROP THE SEAT TO FIT THIS SHELL'S ROOF. Cabin height varies by more
-- than a stud across the library (hyper_x roof sits 0.72 above the
-- body plane, gtr_r34 1.80). A single fixed seat height that looks
-- right in the tallest car pushes the driver's head through the
-- roof of the lowest one, so solve for the seat from the measured
-- roofline instead: seat = roof - head height - clearance.
local seatPart = model:FindFirstChild(Chassis.SEAT_NAME)
if seatPart and rawSize then
-- The roofline is NOT the top of the shell's bounding box. Every
-- library mesh carries junk above the cabin -- gtr_r34 has a stray
-- part literally named "Brick" 0.74 studs above its roof, rx7_fc
-- 0.51 -- so measuring to the AABB top invents headroom that isn't
-- there and the head pokes through the actual roof. Take the top of
-- the largest part by volume instead: that is the main body mesh.
local lo3, hi3 = trueAABB(shell)
local roofY = hi3 and hi3.Y or nil
local bestVol = -1
for _, p in ipairs(shell:GetDescendants()) do
if p:IsA("BasePart") then
local vol = p.Size.X * p.Size.Y * p.Size.Z
if vol > bestVol then
bestVol = vol
local cf, sz = p.CFrame, p.Size
roofY = p.Position.Y + 0.5 * (math.abs(cf.RightVector.Y * sz.X)
+ math.abs(cf.UpVector.Y * sz.Y)
+ math.abs(cf.LookVector.Y * sz.Z))
end
end
end
if roofY and lo3 then
local roofLocal = roofY - body.Position.Y
-- HEAD_ABOVE_SEAT was measured from the seat part's CENTRE, but
-- seatLocal below is the pan height (the part is then placed at
-- seatLocal + Size.Y/2). Subtract the half-thickness here or every
-- car quietly loses that much of its intended clearance -- which
-- is why hyper_x measured 0.07 instead of the requested 0.30.
local wanted = roofLocal - Chassis.HEAD_ABOVE_SEAT
- Chassis.ROOF_CLEARANCE - seatPart.Size.Y / 2
-- Allow sinking to the cabin floor: a low roof must win over the
-- nominal seat height, otherwise the clamp silently discards the
-- whole adaptive solve (which is what used to happen). The shell
-- bottom sits SHELL_DROP below the ground plane, so the seat can
-- go slightly under GROUND_OFFSET without breaching the underside
-- -- hyper_x (lowest roof) needs that headroom.
local floor = Chassis.GROUND_OFFSET - SHELL_DROP + 0.1
local seatLocal = math.clamp(
wanted,
floor,
Chassis.GROUND_OFFSET + Chassis.SEAT_ABOVE_GROUND
)
seatPart.CFrame = body.CFrame
* CFrame.new(0, seatLocal + seatPart.Size.Y / 2, 0.2)
-- A WeldConstraint locks in the relative offset it had when it
-- was created, so moving the seat afterwards would be snapped
-- straight back. Rebuild the weld at the new offset.
for _, w in ipairs(body:GetChildren()) do
if w:IsA("WeldConstraint")
and (w.Part1 == seatPart or w.Part0 == seatPart) then
w:Destroy()
end
end
local seatWeld2 = Instance.new("WeldConstraint")
seatWeld2.Part0 = body
seatWeld2.Part1 = seatPart
seatWeld2.Parent = body
end
end
local shellRoot = shell.PrimaryPart or shell:FindFirstChildWhichIsA("BasePart", true)
for _, d in ipairs(shell:GetDescendants()) do
if d:IsA("BasePart") then
@@ -445,8 +595,22 @@ function CarManager.spawn(player: Player, carId: string?)
end
AntiCheat.grant(player)
-- Driver ignores the chassis BEFORE sitting: the avatar is seated
-- down inside the body slab, and overlapping colliders at Sit() is
-- what launches characters.
setCharacterGroup(character, DRIVER_GROUP)
seat:Sit(humanoid)
-- Restore normal collision when they get out, otherwise the player
-- walks around permanently unable to touch cars.
local seatConn: RBXScriptConnection
seatConn = seat:GetPropertyChangedSignal("Occupant"):Connect(function()
if seat.Occupant == nil then
setCharacterGroup(character, "Default")
seatConn:Disconnect()
end
end)
task.defer(function()
task.wait(SETTLE_TIME) -- let Sit() + the solver settle
if not primary:IsDescendantOf(Workspace) then
+50 -5
View File
@@ -37,12 +37,57 @@ Chassis.STEER_SIGN = -1 -- steer right (Steer = 1) needs negative yaw about +Y
-- 12-stud chassis forced those meshes to be scaled DOWN ~26%, which left the
-- driver sticking out of the roof. Chassis now matches the meshes at 1:1 scale.
Chassis.BODY_SIZE = Vector3.new(6.6, 1.6, 17.0)
Chassis.WHEEL_RADIUS = 2.0
Chassis.WHEEL_WIDTH = 1.4
Chassis.TRACK_HALF = 3.7 -- lateral distance body centre -> wheel centre
-- Wheel size is set from the shell length, not picked by eye. A real car's
-- tyre diameter is ~14% of its length; at SHELL_LENGTH 19 that is ~2.7 studs.
-- The old 4.0-stud tyre (21%) is what made every car read as a monster truck,
-- and because GROUND_OFFSET is derived from it the whole body rode 4.2 studs
-- up in the air to clear it.
Chassis.WHEEL_RADIUS = 1.35
Chassis.WHEEL_WIDTH = 1.1
Chassis.TRACK_HALF = 3.55 -- lateral distance body centre -> wheel centre
Chassis.WHEELBASE_HALF = 5.9 -- longitudinal distance body centre -> axle
Chassis.SUSPENSION_DROP = 2.2 -- rest distance from body centre down to wheel centre
Chassis.SUSPENSION_TRAVEL = 1.2 -- total prismatic travel allowed
Chassis.SUSPENSION_DROP = 1.05 -- rest distance from body centre down to wheel centre
Chassis.SUSPENSION_TRAVEL = 0.9 -- total prismatic travel allowed
-- Cabin fit --------------------------------------------------------------------
-- The driver used to stick out of the roof for THREE compounding reasons, all
-- fixed here + in CarManager:
--
-- 1. SEAT AT ROOF HEIGHT. The Body slab sits at the car's vertical MIDDLE,
-- but the seat was mounted just above it (BODY_SIZE.Y/2 + 0.3). That put
-- the avatar's hips level with the car's waistline and its head a full
-- stud above the roof. A seat belongs on the CABIN FLOOR, which is near
-- the car's BOTTOM -- i.e. measured up from the ground plane, not up from
-- the body slab. SEAT_ABOVE_GROUND does that.
-- 2. SHELLS NEVER SCALED. Imported meshes were parented at native size, which
-- ranges 15.4 -> 19.0 studs long across the library. Cars visibly differed
-- in size and the short ones had no cabin room. SHELL_LENGTH normalises
-- every shell to one length; the mesh's own proportions are preserved
-- because the scale is uniform (Model:ScaleTo).
-- 3. SHELL FLOATED. Placement used ext.Y/2 (a raw extents number that ignores
-- a model's pivot offset -- silv_s15's pivot is 1.45 studs off its own
-- geometry), so shells hovered above the wheels. CarManager now aligns the
-- shell's true bounding box BOTTOM to the ground plane.
--
-- Ground plane, relative to the Body part's centre: the wheels rest on it, so
-- it is one suspension drop plus one wheel radius below the body centre.
Chassis.GROUND_OFFSET = -(Chassis.SUSPENSION_DROP + Chassis.WHEEL_RADIUS)
-- Height of the seat PAN above that ground plane (cabin floor + cushion).
-- This is the STARTING height; CarManager lowers it per-car after the shell is
-- measured (see HEAD_ABOVE_SEAT / ROOF_CLEARANCE), because shells differ in
-- roof height by over a stud (hyper_x 4.92 vs gtr_r34 6.00).
Chassis.SEAT_ABOVE_GROUND = 0.45
-- Measured in Play on a default R15 avatar: the head CENTRE sits 3.27 above the
-- seat pan, but the head is 1.20 tall, so the CROWN is 3.87 up. The old 3.27
-- constant compared a centre against a roofline and so under-measured the
-- avatar by half a head -- the head cleared on paper and poked out on screen.
-- This is deliberately the crown, i.e. the number that must stay under the roof.
Chassis.HEAD_ABOVE_SEAT = 3.90
-- Target gap between the crown and the shell roofline. Now that HEAD_ABOVE_SEAT
-- is a true crown height this is real headroom, so it can be modest.
Chassis.ROOF_CLEARANCE = 0.30
-- Every imported shell is uniformly scaled so its bounding box is this long.
Chassis.SHELL_LENGTH = 19.0
-- Per-car tuning derived from stats -------------------------------------------
export type Tuning = {