Let’s say I would like my platform to take 4 seconds to reach the target, and I also wish that players have the ability to reverse the platforms direction without waiting for it to reach the target. If I’m using TweenService, this creates a problem due to the fact that the time value used to set the duration of the Tween is a constant. This is an example of the problem:
A player is riding an elevator to the top floor, but changes their mind and wishes to go back to the first floor. The elevator is currently midway to the top and takes 8 seconds to reach the top from the first floor. When the player clicks the button to reverse the direction of the elevator, instead of taking 4 seconds to reach the first floor, the elevator will take 8 seconds which isn’t the desired outcome
Why not use PrismaticConstraints then since it solves this issue? While using them will indeed solve it, unfortunately it’s at a cost of creating a new problem: PrismaticConstraints require that the moving platform be unanchored to function, which creates the possibility that an exploiter gains ownership of the platform and the ability to move it wherever their heart desires (If a player is a network owner of an assembly (part for example) and they change its position, it will replicate to the server!). Setting the platform’s network owner to nil (the server) does solve this problem, but at a cost of making the server handle the physics calculations of the platform which isn’t good at all
First create the platform (it should also be Anchored), and add a boolean attribute to that platform named “Up” like so:
Now add a server script and either a ClickDetector or a ProximityPrompt to the platform. Leave the server script blank for now
Inside ReplicatedStorage add a ModuleScript and rename it to “Platform”. Open the editor by double-clicking the module and type the following:
--!strict
local runService = game:GetService"RunService"
local clamp = math.clamp
local platform = {
ServerCreate = function(basePart: BasePart, toggle: ClickDetector | ProximityPrompt)
assert(basePart and basePart:IsA"BasePart", 'The value of "basePart" must be an Instance that inherits from the BasePart class')
assert(toggle and (toggle:IsA"ClickDetector" or toggle:IsA"ProximityPrompt"), 'The value of "toggle" must be an Instance that inherits from either the ClickDetector or the ProximityPrompt class')
basePart.Anchored = true
if toggle:IsA"ClickDetector" then
local default = toggle.MaxActivationDistance
toggle.MouseClick:Connect(function()
toggle.MaxActivationDistance = 0
basePart:SetAttribute("Up", not basePart:GetAttribute"Up")
toggle.MaxActivationDistance = default
end)
else
toggle.Triggered:Connect(function()
toggle.Enabled = false
basePart:SetAttribute("Up", not basePart:GetAttribute"Up")
toggle.ActionText = (basePart:GetAttribute"Up") and "Go Down" or "Go Up"
toggle.Enabled = true
end)
end
end,
ClientCreate = function(basePart: BasePart, time: number, distance: number)
assert(basePart and basePart:IsA"BasePart", 'The value of "basePart" must be an Instance that inherits from the BasePart class')
assert(typeof(time) == "number", 'The value of "time" must be a number')
assert(typeof(distance) == "number", 'The value of "distance" must be a number')
local y = -(1 / time) -- For example: If time equals 2 then the platform will take 2 seconds to reach the goal
local start = basePart.CFrame
local goal = start + start.UpVector * distance
local x = 0
if basePart:GetAttribute"Up" then
y = -y
x = 1
basePart.CFrame = goal
end
local connection
basePart:GetAttributeChangedSignal"Up":Connect(function()
y = -y
if connection then return end -- Else we get a memory leak
connection = runService.PostSimulation:Connect(function(deltaTime: number)
x = clamp(deltaTime * y + x, 0, 1)
basePart.CFrame = start:Lerp(goal, x)
if x == 0 or x == 1 then
connection:Disconnect()
connection = nil
end
end)
end)
end
}
platform.__index = platform
return platform
Now exit the editor and add a LocalScript to StarterPlayerScripts. Open the editor for the new LocalScript and add the following:
local platform = require(game:GetService"ReplicatedStorage".Platform)
platform.ClientCreate(
game:GetService"Workspace":WaitForChild"Part", -- If the platform has a different name, remember to change it!
4, -- How long will the platform take to reach the target
10) -- How high will the platform travel
Now for our final step, open the editor for the server Script we created and type:
local platform = require(game:GetService"ReplicatedStorage".Platform)
platform.ServerCreate(script.Parent, script.Parent.ClickDetector) -- Or script.Parent.ProximityPrompt if you're using one
Now with everything in place, the platform will now behave as shown here in this demonstration I’ve made. It will adjust the speed automatically to maintain a smooth travel, calculate the loop on the client while also having the platform anchored to prevent misuse and is also synchronized between all players in the game
Hopefully you’ve enjoyed my tutorial, and thank you for taking your time to read it
Edit: As @Judgy_Oreo suggested, I’ve changed:
if x == 0 or x == 1 then connection = connection:Disconnect() end
To:
if x == 0 or x == 1 then
connection:Disconnect()
connection = nil
end
To better future-proof the module