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:
+173
-9
@@ -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
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user