How would I optimize or make this interaction system more efficient?

I have a local script that uses RunService.Heartbeat and checks for the closest part named “InteractionPart” in a folder called Interactables. Inside the InteractionPart, there’s a Configuration folder with settings. When it finds a button in range, depending on how many options in the folder (there can be up to 3, as that’s the max the gui will fit), it will display the options to the player.

Here’s a screenshot of how it’s set up (keep in mind the SurfaceGui in the screenshot is the sign itself saying “three options”):

Here’s what it does:
image

A breakdown of each setting:

  • Action: The string that gets sent to the server via RemoteEvent that separates different actions
  • Interactable: A boolvalue that is set on the client. Hides the SurfaceGui when it’s false
  • InteractableBy: A stringvalue that allows only certain players to interact with the button
  • ToolRequired: A stringvalue that, when set, makes it so you can only interact with the button if you’re holding the tool
  • Options folder: Contains up to 3 options, stringvalues with descriptions of the action. The name of the string value (Button1, Button2, Button3) also gets sent to the server via RemoteEvent so the server knows what option you chose

This system works fine. I am only unsatisfied with how clunky it feels. Another downside is I haven’t set up a way so certain options are available to certain people. InteractableBy sets the entire options menu to a certain player. So for example if I wanted to give someone an extra option that’s only available for them, I couldn’t do that without making the entire options menu only visible for them, which defeats the point.

Hey, I’ve read your whole post but think you should give some code as to fulfill the purpose of the subcategory you are posting on but for the system, I cannot see anything wrong with it because purely I don’t know what goes on from your code so otherwise I would think your code is good.

Depending on what you use to find the nearest object, it is possible to optimize it when your method may become a problem if you’re dealing with many interactable objects.

This is the main ModuleScript on the client that deals with the buttons.

Buttons
local Buttons = {}

local Vars = require(game:GetService('ReplicatedStorage').CoreClientModules.ClientVars)
local Lib = require(game:GetService('ReplicatedStorage').CoreClientModules.ClientLib)

local MaxDistance = 6
local ButtonUI
local InteractionPart = nil
local ButtonsTweening = {}
local Connections = {}

local Tweens = {}
local TweenConnections = {}
local ButtonsTweening = {}

function GetInteractionParts()
	local Buttons = {}
	for i,v in pairs(Vars.Interactables:GetDescendants()) do
		if v:IsA('BasePart') and v.Name == 'InteractionPart' then
			table.insert(Buttons, v)
		end
	end
	
	for i,v in pairs(workspace:GetChildren()) do
		if v:FindFirstChild('Torso') and v.Torso:FindFirstChild('Settings') and Vars.Player.Character and v.Torso ~= Vars.Player.Character.Torso then
			table.insert(Buttons, v.Torso)
		end
	end
	
	return Buttons
end

function GetClosestInteractionPart()
	local Buttons = GetInteractionParts()
	local ClosestInteractionPart = nil
	local ClosestDistance = math.huge
	for i,v in pairs(Buttons) do
		local Distance = Vars.Player:DistanceFromCharacter(v.Position)
		if Distance <= MaxDistance and Distance < ClosestDistance then
			ClosestInteractionPart = v
			ClosestDistance = Distance
		end
	end
	
	if ClosestInteractionPart ~= nil then
		return ClosestInteractionPart
	else
		return false
	end
end

function CanInteract(NewInteractionPart)
	local InteractableBy = NewInteractionPart.Settings.InteractableBy
	local ToolRequired = NewInteractionPart.Settings.ToolRequired
	
	if ToolRequired.Value ~= 'None' and not Vars.Player.Character then
		return false
	elseif ToolRequired.Value ~= 'None' and Vars.Player.Character and not Vars.Player.Character:FindFirstChild(ToolRequired.Value) then
		return false
	end
	
	if InteractableBy.Value == 'All' then
		return true
	elseif InteractableBy.Value == Vars.Player.Name then
		return true
	elseif InteractableBy.Value:find(Vars.Player.Team.Name) then
		return true
	else
		return false
	end
end

function TweenButton(Button, TweenType)
	local function TweenOut(ButtonToTween)
		local Goal = {}
		Goal.Size = UDim2.new(1,0,0,0)
		Goal.Position = UDim2.new(0,0,1,0)
		local ResetProgress = Lib.CreateTween(ButtonToTween.Background.ProgressBar, 0, Goal)
		ResetProgress:Play()
		
		Goal = {}
		Goal.Position = UDim2.new(-0.08, 0, -0.08, 0)
		local ButtonOut = Lib.CreateTween(ButtonToTween.Background, 0.2, Goal)
		ButtonOut:Play()
		LastButton = nil
	end
	
	local function TweenIn()
		Goal = {}
		Goal.Position = UDim2.new(0, 0, 0, 0)
		local ButtonIn = Lib.CreateTween(Button.Background, 0.2, Goal)
		ButtonIn:Play()
		
		local Goal = {}
		Goal.Size = UDim2.new(1,0,1,0)
		Goal.Position = UDim2.new(0,0,0,0)
		local Progress = Lib.CreateTween(Button.Background.ProgressBar, 1, Goal)
		Progress:Play()
		return Progress
	end
	
	if TweenType == 'TweenIn' then
		for i,v in pairs(ButtonsTweening) do
			TweenOut(i)
			i = nil
		end
		
		for i,v in pairs(Tweens) do
			i:Cancel()
			i = nil
		end
		
		local Progress = TweenIn()
		Tweens[Progress] = true
		ButtonsTweening[Button] = true
		table.insert(Connections, Progress.Completed:Connect(function(PlaybackState)
			if PlaybackState == Enum.PlaybackState.Completed then
				print('completed!')
				Vars.Remotes.ButtonEvent:FireServer(InteractionPart.Settings.Action.Value, InteractionPart, Button.Name)
				TweenOut(Button)
				Tweens[Progress] = nil
			end
		end))
	elseif TweenType == 'TweenOut' then
		TweenOut(Button)
		ButtonsTweening[Button] = nil
	end
end

function ConnectButtonEvents(ActionName, InputState)
	local Button = nil
	if ActionName == 'Button1' then
		Button = ButtonUI.Frame.Button1
	elseif ActionName == 'Button2' then
		Button = ButtonUI.Frame.Button2
	elseif ActionName == 'Button3' then
		Button = ButtonUI.Frame.Button3
	end
	
	if Button ~= nil then
		if InputState == Enum.UserInputState.Begin then
			TweenButton(Button, 'TweenIn')
		elseif InputState == Enum.UserInputState.End then
			TweenButton(Button, 'TweenOut')
		end
	end
end

function EnableButtonForPlatform(Button)
	local Controller = Vars.UserInputService.GamepadEnabled
	local Touchscreen = Vars.UserInputService.TouchEnabled
	if Controller then
		if Button.Name == 'Button1' then
			Button.Background.Keybind.Text = 'X'
		elseif Button.Name == 'Button2' then
			Button.Background.Keybind.Text = 'B'
		elseif Button.Name == 'Button3' then
			Button.Background.Keybind.Text = 'Y'
		end
	elseif Touchscreen then
		Button.Background.Keybind.Text = ''
		Button.Background.Keybind.TouchIcon.Visible = true
	else
		if Button.Name == 'Button1' then
			Button.Background.Keybind.Text = 'E'
		elseif Button.Name == 'Button2' then
			Button.Background.Keybind.Text = 'F'
		elseif Button.Name == 'Button3' then
			Button.Background.Keybind.Text = 'G'
		end
	end
end

function Refresh()
	for i,v in pairs(Connections) do
		if v.Connected then
			v:Disconnect()
			v = nil
		end
	end
	
	Connections = {}
	ButtonsTweening = {}
	
	for i,v in pairs(ButtonUI.Frame:GetChildren()) do
		if v:IsA('Frame') then
			v:Destroy()
		end
	end
	
	for i = 1,3 do
		Vars.ContextActionService:UnbindAction('Button'..tostring(i))
	end
end

Buttons.Enable = function()
	ButtonUI = script:WaitForChild('ButtonUI'):Clone()
	ButtonUI.Parent = Vars.Player.PlayerGui
	
	Vars.RunService.Heartbeat:Connect(function()
		if Vars.Player.Character then
			local NewInteractionPart = GetClosestInteractionPart()
			if NewInteractionPart and NewInteractionPart ~= InteractionPart and NewInteractionPart.Settings.Interactable.Value and CanInteract(NewInteractionPart) then
				Refresh()
				InteractionPart = NewInteractionPart
				ButtonUI.Adornee = InteractionPart
				
				for i,v in pairs(InteractionPart.Settings.Options:GetChildren()) do
					local NewButton = script.ButtonTemplate:Clone()
					NewButton.Name = v.Name
					EnableButtonForPlatform(NewButton)
					NewButton.Background.Action.Text = v.Value
					NewButton.Parent = ButtonUI.Frame
					
					table.insert(Connections, NewButton.Background.Keybind.MouseButton1Down:Connect(function()
						TweenButton(NewButton, 'TweenIn')
					end))
					
					table.insert(Connections, NewButton.Background.Keybind.MouseButton1Up:Connect(function()
						TweenButton(NewButton, 'TweenOut')
					end))
					
					table.insert(Connections, v.Changed:Connect(function()
						NewButton.Background.Action.Text = v.Value
					end))
	
					
					if v.Name == 'Button1' then
						Vars.ContextActionService:BindAction('Button1', ConnectButtonEvents, false, Enum.KeyCode.E, Enum.KeyCode.ButtonX)
					elseif v.Name == 'Button2' then
						Vars.ContextActionService:BindAction('Button2', ConnectButtonEvents, false, Enum.KeyCode.F, Enum.KeyCode.ButtonB)
					elseif v.Name == 'Button3' then
						Vars.ContextActionService:BindAction('Button3', ConnectButtonEvents, false, Enum.KeyCode.G, Enum.KeyCode.ButtonY)
					end
				end
				
				table.insert(Connections, InteractionPart.Settings.Interactable.Changed:Connect(function()
					if InteractionPart.Settings.Interactable.Value then
						ButtonUI.Adornee = InteractionPart
						ButtonUI.Enabled = true
					else
						ButtonUI.Adornee = nil
						ButtonUI.Enabled = false
					end
				end))
				
				table.insert(Connections, InteractionPart.Settings.InteractableBy.Changed:Connect(function()
					if not CanInteract(InteractionPart) then
						Refresh()
						ButtonUI.Enabled = false
						ButtonUI.Adornee = nil
						InteractionPart = nil
					end
				end))
				
				table.insert(Connections, Vars.Player.Character.ChildRemoved:Connect(function()
					if not CanInteract(InteractionPart) then
						Refresh()
						ButtonUI.Enabled = false
						ButtonUI.Adornee = nil
						InteractionPart = nil
					end
				end))
				
				ButtonUI.Enabled = true
			elseif not NewInteractionPart then
				Refresh()
				ButtonUI.Enabled = false
				ButtonUI.Adornee = nil
				InteractionPart = nil
			end
		end
	end)
end

return Buttons```

You should localize functions when you can. I otherwise see that you could probably shorten the code with some practices like this:

to: return ClosestInteractionPart or false.

But it’s not necessarily going to improve the performance of your code, it’s just better than 5 lines of code in my opinion.

1 Like

Thank you for the suggestion, I will try that.

However, the point of this post is that I’m unsatisfied using a settings folder filled with values instead of something else less clunky. I’ve thought of using CollectionService, but then I realized that would be a nightmare

Tables then would be the next probably but it’d probably just look as clunky.

Object-Oriented Programming is the answer for you. Representing the Interactive Handle as an object in code would eliminate the need for a settings folder, as those can be stored in a ModuleScript somewhere.

Thanks for the reply! Could you give me an example?