How to make a synchronized moving elevator platform using CFrame

First things first: Why use CFrame instead of TweenService or PrismaticConstraints?

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

Using CFrame to move the platform allows you to tackle both problems at once, and here's how:

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 :slightly_smiling_face:

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

4 Likes

Awesome resource! I have a question, does decreasing the time (thus increasing the speed) cause jittering (such as when using TweenService)?

1 Like

I tried setting the speed to 0.25 (even tried 0.125 which caused the platform to move so fast that it phased through me lol) and I’m happy to report that I personally didn’t experience any jittering, even when varying the distance to very small and large numbers

Edit: You will bounce a bit when the platform is traveling down very fast from a high distance though but I don’t think that’s unrealistic because the g-forces involved if you were to do it irl would be super

1 Like

I really don’t think you should depend on :Disconnect not returning anything, outside of compatibility concerns, it isn’t immediately obvious to a reader that this is assigning nil to connection, it’s much easier to understand if you just put connection = nil to another line.

This is a very strange pattern you are doing. None of the code between the assignments to toggle.Enabled are yielding, so at the end of the day you might as well remove toggle.Enabled = false because there is no point in time another script could read it as that (unless you are using the value Default or Immediate for the property workspace.SignalBehavior, which is being phased out).

The server can handle physics, that’s kind of one of it’s main goals of doing other physics and network replication.

Also, for physics objects, Roblox accounts for ping and locally adjusts other player’s positions to reduce clipping or characters looking like they are floating:

I think this is something you should either add to the code or explain as a potential downside, [EDIT:] because without any code changes, players could appear to phase through the platform or float above it.

Outside of those small nitpicks, I think this is pretty good :+1:

I agree, I should be careful about doing this in code I publish, although this is more of a future-proofing concern than a compatibility concern

My thought process for this was as a way to give the server time to processs, although I must admit in this case I don’t think they were really necessary as it isn’t bulky code

This is incorrect (the physics part at least). While the server can handle physics, for performance reasons you shouldn’t rely on it to do especially for cosmetic actions like an elevator

I explained the reason why I’m not using PrismaticConstraints in my post, it’s due to security like so:

1 Like

I was just pointing it out as something you could mention in the post as a downside or add for feature parity:

That issue is caused due to desynchronization if the loop moving the platform is running in the server though, which isn’t a problem in my case since the loop moving the platform is running in the client

I can confirm this because I tested the system under load with multiple clients and there wasn’t any desynchronization occurring where players appear to phase through the platform