M1: constraint chassis (springs, servo steering, motor wheels)
CI / quality (push) Successful in 54s
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:
@@ -0,0 +1,133 @@
|
||||
--!strict
|
||||
--[[
|
||||
ChassisController.luau (CLIENT)
|
||||
Drives the constraint chassis while the local player is in a DriveSeat.
|
||||
|
||||
Why client-side: the server gives the driver network ownership of the car,
|
||||
so physics (and constraint targets) run authoritatively on THIS client —
|
||||
that's what makes driving feel responsive instead of laggy. The server
|
||||
still owns WHICH car you get (allowlist etc.); this module only turns
|
||||
seat input into motor/servo targets.
|
||||
|
||||
Loop per Heartbeat while seated:
|
||||
Steer -> front servo TargetAngle (speed-sensitive: less lock when fast)
|
||||
Throttle-> wheel motor AngularVelocity + MotorMaxTorque
|
||||
No input-> brake torque toward zero angular velocity
|
||||
]]
|
||||
|
||||
local Players = game:GetService("Players")
|
||||
local RunService = game:GetService("RunService")
|
||||
local ReplicatedStorage = game:GetService("ReplicatedStorage")
|
||||
|
||||
local Shared = ReplicatedStorage.Shared
|
||||
local Cars = require(Shared.Cars)
|
||||
local Chassis = require(Shared.Chassis)
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
|
||||
local ChassisController = {}
|
||||
|
||||
local heartbeatConn: RBXScriptConnection? = nil
|
||||
local seatedConn: RBXScriptConnection? = nil
|
||||
|
||||
local function findCarModel(seat: VehicleSeat): Model?
|
||||
local model = seat:FindFirstAncestorOfClass("Model")
|
||||
if model and model:GetAttribute("CarId") ~= nil then
|
||||
return model
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function collectConstraints(model: Model): ({ HingeConstraint }, { HingeConstraint })
|
||||
local motors: { HingeConstraint } = {}
|
||||
local servos: { HingeConstraint } = {}
|
||||
for _, desc in ipairs(model:GetDescendants()) do
|
||||
if desc:IsA("HingeConstraint") then
|
||||
if desc.Name == Chassis.WHEEL_MOTOR_NAME then
|
||||
table.insert(motors, desc)
|
||||
elseif desc.Name == Chassis.STEER_SERVO_NAME then
|
||||
table.insert(servos, desc)
|
||||
end
|
||||
end
|
||||
end
|
||||
return motors, servos
|
||||
end
|
||||
|
||||
local function startDriving(seat: VehicleSeat)
|
||||
local model = findCarModel(seat)
|
||||
if not model then
|
||||
return
|
||||
end
|
||||
local carId = model:GetAttribute("CarId") :: string
|
||||
local car = Cars.Get(carId)
|
||||
if not car then
|
||||
return
|
||||
end
|
||||
local tuning = Chassis.tuningFor(car)
|
||||
local motors, servos = collectConstraints(model)
|
||||
local body = model.PrimaryPart
|
||||
if #motors == 0 or not body then
|
||||
return
|
||||
end
|
||||
|
||||
heartbeatConn = RunService.Heartbeat:Connect(function()
|
||||
local throttle = seat.ThrottleFloat
|
||||
local steer = seat.SteerFloat
|
||||
|
||||
-- Steering: speed-sensitive — at top speed only ~40% of full lock,
|
||||
-- so the car doesn't spin out from a tap of the key at 150 studs/s.
|
||||
local speed = body.AssemblyLinearVelocity.Magnitude
|
||||
local speedFrac = math.clamp(speed / tuning.MaxSpeed, 0, 1)
|
||||
local lockScale = 1 - 0.6 * speedFrac
|
||||
local targetAngle = Chassis.STEER_SIGN * steer * tuning.MaxSteerAngle * lockScale
|
||||
for _, servo in ipairs(servos) do
|
||||
servo.TargetAngle = targetAngle
|
||||
end
|
||||
|
||||
-- Drive/brake:
|
||||
if math.abs(throttle) > 0.05 then
|
||||
local targetVel = Chassis.DRIVE_SIGN * throttle * tuning.MaxWheelAngularVelocity
|
||||
for _, motor in ipairs(motors) do
|
||||
motor.AngularVelocity = targetVel
|
||||
motor.MotorMaxTorque = tuning.MotorMaxTorque
|
||||
end
|
||||
else
|
||||
-- Engine braking: drive wheel speed toward zero with brake torque.
|
||||
for _, motor in ipairs(motors) do
|
||||
motor.AngularVelocity = 0
|
||||
motor.MotorMaxTorque = tuning.BrakeTorque
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function stopDriving()
|
||||
if heartbeatConn then
|
||||
heartbeatConn:Disconnect()
|
||||
heartbeatConn = nil
|
||||
end
|
||||
end
|
||||
|
||||
function ChassisController.init()
|
||||
-- SeatPart changes whenever the humanoid sits down or gets up.
|
||||
local function onCharacter(character: Model)
|
||||
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
|
||||
if seatedConn then
|
||||
seatedConn:Disconnect()
|
||||
end
|
||||
seatedConn = humanoid:GetPropertyChangedSignal("SeatPart"):Connect(function()
|
||||
stopDriving()
|
||||
local seatPart = humanoid.SeatPart
|
||||
if seatPart and seatPart:IsA("VehicleSeat") and seatPart.Name == Chassis.SEAT_NAME then
|
||||
startDriving(seatPart)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if player.Character then
|
||||
onCharacter(player.Character)
|
||||
end
|
||||
player.CharacterAdded:Connect(onCharacter)
|
||||
end
|
||||
|
||||
return ChassisController
|
||||
Reference in New Issue
Block a user