A sliding door opening/closing issue when a lot of players interact with it at the same time

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve?
    I want to achieve a watertight door that opens and closes. The idea is:
    Player triggers a prompt on the Handle. The Handle starts to spin, and so does a pole with 2 cogwheel models. While they spin, the door model slides open/close. I attached a video below how it works. In most of the cases, it works fine.

  2. What is the issue? Include screenshots / videos if possible!
    The issue is that if a lot of players attempt to interact with the prompt all at once, there is a chance that the door will break. It will get stuck half-way open, and the pole and handle will just spin in an endless loop (in some cases they don’t). Video provided below. To be clear, it’s pretty hard to replicate that glitch in studio - I had to use an autoclicker with 100 or sometimes 10 ms interval. And even more to that, I had to catch a right moment to stop clicking, in order to cause this bug.

  3. What solutions have you tried so far? Did you look for solutions on the Creator Hub?
    I have tried searching on dev forum, and I tried adding more strict flags to prevent 2 tweens from overlapping. Didn’t help.

Important information (I guess):
In the game itself, the prompt takes 2 seconds to trigger, and gets disabled for the time of tweening. However, the issue still occurs.

This is my current script

local watertightDoorFolder = script.Parent
local door = watertightDoorFolder:WaitForChild("Door")
local pole = watertightDoorFolder:WaitForChild("Pole")
local handle = watertightDoorFolder:WaitForChild("WTDHandle")
local prompt = watertightDoorFolder:FindFirstChildWhichIsA("ProximityPrompt", true)

local TweenService = game:GetService("TweenService")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local WTDCloseEvent = ReplicatedStorage:WaitForChild("WTDCloseEvent")

local isOpen = false
local spinning = false
local busy = false --prevent double activation

local tweenInfo = TweenInfo.new(2, Enum.EasingStyle.Sine, Enum.EasingDirection.InOut)

local function tweenDoor(targetOffset)
	local doorPrimaryCFrame = door:GetPrimaryPartCFrame()
	local startCFrame = doorPrimaryCFrame
	local targetCFrame = startCFrame * targetOffset

	local numberValue = Instance.new("NumberValue")
	numberValue.Value = 0
	local tween = TweenService:Create(numberValue, tweenInfo, { Value = 1 })

	local doorParts = {}
	local relativeOffsets = {}

	for _, part in ipairs(door:GetDescendants()) do
		if part:IsA("BasePart") then
			table.insert(doorParts, part)
			-- Save the offset from PrimaryPart
			relativeOffsets[part] = door.PrimaryPart.CFrame:toObjectSpace(part.CFrame)
		end
	end

	numberValue.Changed:Connect(function(alpha)
		local newCFrame = startCFrame:Lerp(targetCFrame, alpha)

		for _, part in ipairs(doorParts) do
			local relOffset = relativeOffsets[part]
			part.CFrame = newCFrame * relOffset
		end
	end)

	tween:Play()
	return tween
end


local function spinPole(direction)
	spinning = true
	local speed = math.rad(90)
	if direction == "close" then
		speed = -speed
	end

	local cogwheelModels = {}
	for _, model in ipairs(pole:GetChildren()) do
		if model:IsA("Model") and model.PrimaryPart then
			table.insert(cogwheelModels, model)
		elseif model:IsA("Model") then
			warn("Cogwheel model "..model.Name.." is missing a PrimaryPart!")
		end
	end

	local spinningPartsModel = handle:FindFirstChild("SpinningParts")
	local handleSpinningModel = spinningPartsModel and spinningPartsModel:IsA("Model") and spinningPartsModel.PrimaryPart and spinningPartsModel or nil
	if not handleSpinningModel and spinningPartsModel then
		warn("Handle's SpinningParts model is missing a PrimaryPart!")
	end

	local connection
	connection = RunService.Heartbeat:Connect(function(deltaTime)
		if not spinning then
			connection:Disconnect()
			return
		end

		pole.CFrame = pole.CFrame * CFrame.Angles(speed * deltaTime, 0, 0)

		for _, model in ipairs(cogwheelModels) do
			model:PivotTo(model:GetPivot() * CFrame.Angles(speed * deltaTime * 3, 0, 0))
		end

		if handleSpinningModel then
			handleSpinningModel:PivotTo(handleSpinningModel:GetPivot() * CFrame.Angles(speed * deltaTime * 2, 0, 0))
		end
	end)
end

task.delay(20, function()
	spinPole("open")
	local doorTween = tweenDoor(CFrame.new(0, 0, -3.162))
	doorTween.Completed:Wait()
	spinning = false
	isOpen = true
	prompt.ActionText = "Close"
end)

WTDCloseEvent.Event:Connect(function()
	spinPole("close")
	local doorTween = tweenDoor(CFrame.new(0, 0, 3.162))
	doorTween.Completed:Wait()
	spinning = false
	isOpen = false
	prompt.ActionText = "Open"
end)

prompt.Triggered:Connect(function(player)
	if busy then
		return
	end

	busy = true
	prompt.Enabled = false

	if not isOpen then
		spinPole("open")
		local doorTween = tweenDoor(CFrame.new(0, 0, -3.162))
		doorTween.Completed:Wait()
		spinning = false
		isOpen = true
		prompt.ActionText = "Close"
	else
		spinPole("close")
		local doorTween = tweenDoor(CFrame.new(0, 0, 3.162))
		doorTween.Completed:Wait()
		spinning = false
		isOpen = false
		prompt.ActionText = "Open"
	end

	prompt.Enabled = true
	busy = false
end)

If you have any questions or if you need some information, feel free to ask!

4 Likes

When one player interacts with the prompt, send a remote event to every player besides the interact-er that disables that prompt in a local script. Once the interaction is done, resend that remote event to reenable the prompt on all players. This makes it so that only one player can use it at a time.

Also, on the serverside, make sure that it only accepts one player at a time.

2 Likes

Disable the prompt when it is being used; it acts as a debounce. An alternative solution would be to keep the prompt enabled and add a debounce.

1 Like

It does get disabled in the main game, but the bug still occurs (I turned it off for the video to demonstrate how the bug works)

2 Likes

Instead of calculating the goal cframe relative to its current position, cache the start and end cframe at the beginning of the script and re-use it.

1 Like

If you want to keep the logic you currently have, you can first create the tweens in a table near the start of the script:

local doorTweens = {
    doorOpen = TweenService:Create(),
    doorClose = TweenService:Create(),
}

then you can call these instead of creating a new tween each time. Before calling the function, run a for loop that stops all tweens in the table.

So:

--> When calling 'Door Open'

for i, tween in pairs(doorTweens) do
    tween:Stop()
end
doorTween.doorOpen:Play()

This can definitely be improved on but would be best if you want to add more states than just opened or closed (but you probably won’t, lol)

If you’re just gonna keep open or closed, you can just:

--> Setup
local isOpen = false
local doorTweens = {
    [true] = TS:Create(), --> Door Open
    [false] = TS:Create() --> Door Close
}

--> Opening doors, for example

isOpen = true
doorTweens[not isOpen]:Stop() 
doorTweens[isOpen]:Play()

Also, since the last two lines will always stay the same, you can just set isOpen to true or false and condense the tween playing lines into one function that you can call afterwards.

1 Like

I know. But that doesn’t work in my case btw - the door changes position and orientation several times throught the gameplay,

You need to calculate the goal cframe based on something that is relatively constant. Calculating the goal cframe relative to the moving door is not the way to go. If the whole system is moving, use something that is more reliable, the door frame for example.

I had just integrated this option in my prompt.Triggered script

prompt.Triggered:Connect(function(player)
	if busy then
		return
	end

	busy = true
	prompt.Enabled = false

	-- Disable prompt for all other clients
	for _, plr in ipairs(Players:GetPlayers()) do
		if plr ~= player then
			WTDPromptControl:FireClient(plr, "SetPrompt", watertightDoorFolder.Name, false)
		end
	end

	if not isOpen then
		spinPole("open")
		local doorTween = tweenDoor(CFrame.new(0, 0, -3.162))
		doorTween.Completed:Wait()
		spinning = false
		isOpen = true
		prompt.ActionText = "Close"
	else
		spinPole("close")
		local doorTween = tweenDoor(CFrame.new(0, 0, 3.162))
		doorTween.Completed:Wait()
		spinning = false
		isOpen = false
		prompt.ActionText = "Open"
	end

	-- Re-enable prompt for all players
	for _, plr in ipairs(Players:GetPlayers()) do
		if plr ~= player then
			WTDPromptControl:FireClient(plr, "SetPrompt", watertightDoorFolder.Name, true)
		end
	end

	prompt.Enabled = true
	busy = false
end)
-- LocalScript in StarterPlayerScripts
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local WTDPromptControl = ReplicatedStorage:WaitForChild("WTDPromptControl")

WTDPromptControl.OnClientEvent:Connect(function(action, doorName, isEnabled)
	local doorModel = workspace:FindFirstChild(doorName)
	if doorModel then
		local prompt = doorModel:FindFirstChildWhichIsA("ProximityPrompt", true)
		if prompt then
			prompt.Enabled = isEnabled
		end
	end
end)

The issue still occurs, but from what I saw (or at least it seems to me), that it became less common now. I think I can’t fully get rid of it, because the prompt still takes a few ms to disable either way, and if someone else happens to press during that delay, the tweens will overlap.

Creating the tweens at the start of the script instead each time can help with delay. Even if you’re not gonna keep that logic, the thing I posted can still help!

Update on the situation: I decided to scrap the interactable doors, and made them single-use (basically after you open the door, the prompt gets destroyed and you cant do anything with it). However, I’ll still mark the first comment as solution, because it does decrease the chance of that happening, and if you have the same door system that doesn’t change position/orientation, I’m sure that solution may be helpful.
Thank you for your help, everyone! I really appreciate it ^^

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.