If you lerp the CFrame it wont snap (with or without a physical constraint), as the CFrame’s update over every frame would be smooth
So, I decided to make a full example script on how CFrames can be used to make smooth movement
It has a lot of CFrame manipulations which are probably hard to understand. You don’t need to focus on that (if you use a bezier curve for your train, the bezier curve function will spit out a CFrame directly after you give the alpha value to the function, so it’s kinda simpler?)
What is important is the alpha value. The alpha value is calculated using time (os.clock()
here), so on every frame, the alpha value gives you the completion % of the tween, and even with inconsistent fps, since it uses time, it will still be smooth. Lerp on CFrames is also really useful, you can’t lerp orientation like that
local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local Door = workspace.Door
local ClickDetector = Door.ClickDetector
local DoorRadius = Door.Size.X/2
local HingeOffset = CFrame.new(-DoorRadius, 0, 0) -- Offset from the center of the door to the hinge
local DoorHingeCFrameClosed = Door.CFrame * HingeOffset
local DoorHingeCFrameOpen = DoorHingeCFrameClosed * CFrame.Angles(0, math.pi/2, 0) -- math.pi/2 is 90°, but in radians
local TweenDuration = .5
local function TweenDoor(StartCFrame : CFrame, EndCFrame : CFrame)
local StartTick = os.clock()
-- Return a number from 0 to 1, 0 is 0% of the tween completed, 100% is the tween is done
local function GetAlpha()
return math.clamp((os.clock() - StartTick)/TweenDuration, 0, 1)
end
--
-- Unbind previously binded function
RunService:UnbindFromRenderStep("DoorTween")
RunService:BindToRenderStep("DoorTween", Enum.RenderPriority.Camera.Value -1, function()
local alpha = GetAlpha()
-- This line allows to apply easing styles to the alpha value, it basically remaps the alpha value following an "easing function"
--alpha = TweenService:GetValue(alpha, Enum.EasingStyle.Elastic, Enum.EasingDirection.Out)
local HingeCFrame = StartCFrame:Lerp(EndCFrame, alpha)
Door.CFrame = HingeCFrame * HingeOffset:Inverse() -- Need to go from hinge to door, rather than door to hinge, so need to invert the offset
if alpha == 1 then RunService:UnbindFromRenderStep("DoorTween") end
end)
end
local IsOpen = false
ClickDetector.MouseClick:Connect(function()
IsOpen = not IsOpen
-- Need to get the current hinge CFrame, if the door was still being tweened when clicked
local CurrentHingeCFrame = Door.CFrame * HingeOffset
if IsOpen then
TweenDoor(CurrentHingeCFrame, DoorHingeCFrameOpen)
else
TweenDoor(CurrentHingeCFrame, DoorHingeCFrameClosed)
end
end)
The door is just a simple part, with a ClickDetector inside