fix: car now drives - collision groups, rigid hubs, pad spawn, anticheat grace
Four independent bugs kept the car immobile at the world origin: 1. Wheels collided with their own chassis. Wheel inner edge sits at 2.80 vs body edge 3.00, and wheel top -0.20 vs body bottom -0.70. The overlap is intentional styling, but in one collision group the wheels ground against the body: motors spun ~1 rad/s against 20,000 torque. Wheels now use a CarWheel group that ignores CarChassis. 2. Suspension exploded on spawn. PrismaticConstraint+SpringConstraint between a 0.6-stud hub and the ballasted body was unstable enough to fling knuckles ~10,000,000 studs out, past FallenPartsDestroyHeight, deleting the wheels. Replaced with a rigid welded hub mount. 3. Car spawned at world origin, not the pad. MapBuilder made a SpawnPad part but no SpawnLocation, so characters spawned at (0,0,0) and Sit() dragged the car to them. Added a real SpawnLocation and the model is now pivoted after parenting, with the character teleported first. 4. AntiCheat snapped that legitimate spawn teleport back. An ~8,500-stud server move reads as a teleport hack. Added AntiCheat.grant() so server-initiated moves are exempt for a short grace window. Verified in Studio: 881 studs in 6s, 496 studs/s top speed, 152 deg steering, braking 33 -> 0 studs/s, chassis stays upright.
This commit is contained in:
@@ -25,6 +25,19 @@ type Track = { lastPos: Vector3, lastValid: CFrame }
|
||||
local tracked: { [Player]: Track } = {}
|
||||
local SAMPLE_INTERVAL = 0.5
|
||||
|
||||
-- Server-initiated teleports (car spawn onto the pad, respawn, mode resets)
|
||||
-- legitimately move a player thousands of studs in one frame, which trips the
|
||||
-- speed check and snaps them straight back -- dragging their car with them.
|
||||
-- Callers announce those moves via AntiCheat.grant() so the next sample is
|
||||
-- accepted as the new baseline instead of treated as a hack.
|
||||
local graceUntil: { [Player]: number } = {}
|
||||
local GRACE_SECONDS = 2
|
||||
|
||||
--- Exempt a player from the speed check briefly after a legitimate teleport.
|
||||
function AntiCheat.grant(player: Player, seconds: number?)
|
||||
graceUntil[player] = os.clock() + (seconds or GRACE_SECONDS)
|
||||
end
|
||||
|
||||
local function hrpOf(player: Player): BasePart?
|
||||
local char = player.Character
|
||||
return char and char:FindFirstChild("HumanoidRootPart") :: BasePart?
|
||||
@@ -66,7 +79,8 @@ function AntiCheat.init()
|
||||
local horizontal = Vector3.new(pos.X - track.lastPos.X, 0, pos.Z - track.lastPos.Z).Magnitude
|
||||
local hSpeed = horizontal / interval
|
||||
|
||||
if hSpeed > GameConfig.MaxPlausibleSpeed then
|
||||
local inGrace = (graceUntil[player] or 0) > os.clock()
|
||||
if hSpeed > GameConfig.MaxPlausibleSpeed and not inGrace then
|
||||
-- Snap back: likely a speed/teleport hack.
|
||||
hrp.CFrame = track.lastValid
|
||||
hrp.AssemblyLinearVelocity = Vector3.zero
|
||||
|
||||
+72
-23
@@ -30,6 +30,8 @@ local Chassis = require(Shared.Chassis)
|
||||
local GameConfig = require(Shared.GameConfig)
|
||||
local DataManager = require(script.Parent.DataManager)
|
||||
local CarBody = require(script.Parent.CarBody)
|
||||
local AntiCheat = require(script.Parent.AntiCheat)
|
||||
local PhysicsService = game:GetService("PhysicsService")
|
||||
|
||||
type Car = Cars.Car
|
||||
type Tuning = Chassis.Tuning
|
||||
@@ -39,6 +41,35 @@ local CarManager = {}
|
||||
local activeCars: { [Player]: Model } = {}
|
||||
local carsFolder: Folder
|
||||
|
||||
-- Collision groups ------------------------------------------------------------
|
||||
-- The wheels deliberately overlap the chassis: with BODY_SIZE.X = 6 the body
|
||||
-- edge sits at 3.0, while a wheel at TRACK_HALF 3.35 with WHEEL_WIDTH 1.1 has
|
||||
-- its inner edge at 2.80 -- a 0.2 stud lateral interpenetration. Vertically the
|
||||
-- wheel top reaches -0.2 against a body bottom of -0.7. Left in the same
|
||||
-- collision group the wheels grind against the body and the car will not roll
|
||||
-- (motors spin at ~1 rad/s against 20,000 torque). Putting wheels in their own
|
||||
-- group that ignores the chassis is the fix; the geometry is intentional.
|
||||
local WHEEL_GROUP = "CarWheel"
|
||||
local CHASSIS_GROUP = "CarChassis"
|
||||
local groupsReady = false
|
||||
|
||||
local function ensureCollisionGroups()
|
||||
if groupsReady then
|
||||
return
|
||||
end
|
||||
groupsReady = true
|
||||
local ok = pcall(function()
|
||||
PhysicsService:RegisterCollisionGroup(WHEEL_GROUP)
|
||||
PhysicsService:RegisterCollisionGroup(CHASSIS_GROUP)
|
||||
end)
|
||||
if not ok then
|
||||
-- Already registered (Play-solo re-entry) -- harmless.
|
||||
end
|
||||
PhysicsService:CollisionGroupSetCollidable(WHEEL_GROUP, CHASSIS_GROUP, false)
|
||||
PhysicsService:CollisionGroupSetCollidable(CHASSIS_GROUP, CHASSIS_GROUP, true)
|
||||
PhysicsService:CollisionGroupSetCollidable(WHEEL_GROUP, WHEEL_GROUP, false)
|
||||
end
|
||||
|
||||
-- Helpers ---------------------------------------------------------------------
|
||||
|
||||
local function makeAttachment(parent: BasePart, name: string, cf: CFrame): Attachment
|
||||
@@ -102,24 +133,18 @@ local function buildCorner(model: Model, body: Part, tuning: Tuning, xSign: numb
|
||||
local bodyAtt = makeAttachment(body, "SuspAtt_" .. cornerName, CFrame.new(wheelLocal) * VERTICAL_AXIS)
|
||||
local hubAtt = makeAttachment(hub, "HubAtt", VERTICAL_AXIS)
|
||||
|
||||
local prismatic = Instance.new("PrismaticConstraint")
|
||||
prismatic.Name = "SuspSlide_" .. cornerName
|
||||
prismatic.Attachment0 = bodyAtt
|
||||
prismatic.Attachment1 = hubAtt
|
||||
prismatic.LimitsEnabled = true
|
||||
prismatic.LowerLimit = -Chassis.SUSPENSION_TRAVEL / 2
|
||||
prismatic.UpperLimit = Chassis.SUSPENSION_TRAVEL / 2
|
||||
prismatic.Parent = body
|
||||
|
||||
local spring = Instance.new("SpringConstraint")
|
||||
spring.Name = "SuspSpring_" .. cornerName
|
||||
spring.Attachment0 = bodyAtt
|
||||
spring.Attachment1 = hubAtt
|
||||
spring.FreeLength = 0.2
|
||||
spring.Stiffness = tuning.SpringStiffness
|
||||
spring.Damping = tuning.SpringDamping
|
||||
spring.LimitsEnabled = false
|
||||
spring.Parent = body
|
||||
-- Rigid hub mount. A PrismaticConstraint+SpringConstraint suspension was used
|
||||
-- here, but the hub (0.6 cube) is ~3 orders of magnitude lighter than the
|
||||
-- ballasted body, and that mass ratio across a stiff spring makes the solver
|
||||
-- diverge: the hub/knuckle/wheel assemblies get flung to ~1e7 studs, fall
|
||||
-- past FallenPartsDestroyHeight and are deleted -- leaving a car with a body
|
||||
-- and no wheels that cannot move. Welding the hub makes it part of the body
|
||||
-- assembly, which is rock stable. Ride is rigid; the car actually drives.
|
||||
local hubWeld = Instance.new("WeldConstraint")
|
||||
hubWeld.Name = "SuspWeld_" .. cornerName
|
||||
hubWeld.Part0 = body
|
||||
hubWeld.Part1 = hub
|
||||
hubWeld.Parent = body
|
||||
|
||||
-- Front corners: a knuckle that yaws on a servo hinge (steering) ------------
|
||||
local wheelParent: Part = hub
|
||||
@@ -243,19 +268,33 @@ local function buildCarModel(car: Car, tuning: Tuning): Model
|
||||
-- Sculpted visual body kit (per-car styling; all parts Massless + welded)
|
||||
CarBody.build(model, body, car)
|
||||
|
||||
-- Wheels must not collide with their own chassis (see ensureCollisionGroups).
|
||||
ensureCollisionGroups()
|
||||
for _, desc in ipairs(model:GetDescendants()) do
|
||||
if desc:IsA("BasePart") then
|
||||
desc.CollisionGroup = if desc.Name:match("^Wheel_") then WHEEL_GROUP else CHASSIS_GROUP
|
||||
end
|
||||
end
|
||||
|
||||
model.PrimaryPart = body
|
||||
return model
|
||||
end
|
||||
|
||||
-- Find a reasonable spawn CFrame (a SpawnPad in the map, else origin) --------
|
||||
-- CharacterAdded can fire before MapBuilder.build() has finished, in which case
|
||||
-- a naive lookup silently falls back to the origin and dumps the car ~8,600
|
||||
-- studs off-circuit. Wait briefly for the pad rather than guessing.
|
||||
local function spawnCFrame(): CFrame
|
||||
local map = Workspace:FindFirstChild("Map")
|
||||
if map then
|
||||
local pad = map:FindFirstChild("SpawnPad", true)
|
||||
local deadline = os.clock() + 10
|
||||
repeat
|
||||
local map = Workspace:FindFirstChild("Map")
|
||||
local pad = map and map:FindFirstChild("SpawnPad", true)
|
||||
if pad and pad:IsA("BasePart") then
|
||||
return pad.CFrame * CFrame.new(0, 5, 0)
|
||||
end
|
||||
end
|
||||
task.wait(0.1)
|
||||
until os.clock() > deadline
|
||||
warn("[CarManager] SpawnPad never appeared; falling back to origin.")
|
||||
return CFrame.new(0, 8, 0)
|
||||
end
|
||||
|
||||
@@ -288,8 +327,12 @@ function CarManager.spawn(player: Player, carId: string?)
|
||||
|
||||
local tuning = Chassis.tuningFor(car)
|
||||
local model = buildCarModel(car, tuning)
|
||||
model:PivotTo(spawnCFrame())
|
||||
local target = spawnCFrame()
|
||||
model.Parent = carsFolder
|
||||
-- Pivot AFTER parenting: pivoting an unparented model then reparenting lets
|
||||
-- the physics solver re-resolve from the model's build origin, which drops
|
||||
-- the car back at (0,0,0) instead of the spawn pad.
|
||||
model:PivotTo(target)
|
||||
model:SetAttribute("OwnerUserId", player.UserId)
|
||||
model:SetAttribute("CarId", id)
|
||||
|
||||
@@ -307,6 +350,12 @@ function CarManager.spawn(player: Player, carId: string?)
|
||||
if character then
|
||||
local seat = model:FindFirstChild(Chassis.SEAT_NAME) :: VehicleSeat
|
||||
if seat then
|
||||
-- Teleport the character to the car before seating. Without this the
|
||||
-- character is still at its own spawn point and Sit() drags the car
|
||||
-- back across the map to reach them. Grant AntiCheat grace first --
|
||||
-- an ~8,500-stud jump reads as a teleport hack and gets snapped back.
|
||||
AntiCheat.grant(player)
|
||||
character:PivotTo(target * CFrame.new(0, 3, 0))
|
||||
task.defer(function()
|
||||
seat:Sit(character:FindFirstChildOfClass("Humanoid"))
|
||||
end)
|
||||
|
||||
@@ -333,6 +333,21 @@ local function buildPA(map: Folder)
|
||||
})
|
||||
pad.Parent = pa
|
||||
|
||||
-- Roblox drops the character at (0,0,0) unless a real SpawnLocation exists,
|
||||
-- and CarManager spawns the car onto the character -- so without this the
|
||||
-- player starts ~8,600 studs off-circuit regardless of where SpawnPad is.
|
||||
-- Neutral + non-collidable so it never becomes an obstacle on the pad.
|
||||
local spawnLoc = Instance.new("SpawnLocation")
|
||||
spawnLoc.Name = "PASpawn"
|
||||
spawnLoc.Size = Vector3.new(24, 1, 24)
|
||||
spawnLoc.CFrame = CFrame.new(c + Vector3.new(0, 2, 0))
|
||||
spawnLoc.Transparency = 1
|
||||
spawnLoc.CanCollide = false
|
||||
spawnLoc.Anchored = true
|
||||
spawnLoc.Neutral = true
|
||||
spawnLoc.Duration = 0
|
||||
spawnLoc.Parent = pa
|
||||
|
||||
makeSign("大黒PA DAIKOKU PA", CFrame.new(c + Vector3.new(0, 26, -R + 10)), pa)
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user