I made a pretty clean interaction system using modules scripts & collection service!

the dialogue is a modified version of: https://www.roblox.com/library/6453289199/Dialogue-Tools

This was my first time using collection service and I think it turned out pretty good.

Code:

Server Script

local collectionService = game:GetService("CollectionService")
local tween = require(game.ReplicatedStorage.modules.tween)

for _, part in pairs(collectionService:GetTagged("interaction")) do
	local interactGui = game:GetService("ReplicatedStorage").interact:Clone()
	interactGui.Parent = part
	
	local inputRem = game.ReplicatedStorage.input
	
	inputRem.OnServerEvent:Connect(function(player, input, camCFrame)
		if (part.Position - camCFrame).magnitude < 15 then
			local tweenGoals = {
				BackgroundColor3 = Color3.new(1, 1, 1)
			}
			
			tween.Tween(interactGui.Frame.interactKeyBG, 0.05, "Sine", "InOut", tweenGoals)
			wait(0.1)	
			local tweenGoals = {
				BackgroundColor3 = Color3.new(0.270588, 0.270588, 0.270588)
			}
			
			tween.Tween(interactGui.Frame.interactKeyBG, 0.1, "Sine", "InOut", tweenGoals)
			
			local interact = require(part.interact)
			interact:interact(player)
		end
	end)
end

Client Script

local uis = game:GetService("UserInputService")

uis.InputBegan:Connect(function(input, GameProcessed)
	if input.KeyCode == Enum.KeyCode.F then
		local camCFrame = game:GetService("Workspace").CurrentCamera.CFrame.Position
		game.ReplicatedStorage.input:FireServer(input, camCFrame)
	end
end)

module script (within all parts that are interacted with)

local module = {}

function module:interact(player)
	game.ReplicatedStorage.DialogueRemote:FireClient(player, script.Parent.Dialogue)
end

return module

8 Likes

Looks good, thanks for sharing.

It is definitely bad practice to tween UI on the server. As a general note, it would be much better to handle this completely locally. It doesn’t make sense to needlessly forward arguments between the client and the server when the client doesn’t need data from the server.
A much better way to do this is to store messages/text data in a ModuleScript and then have a LocalScript handle interactions locally with the available data.

Other feedback:

uis.InputBegan:Connect(function(input, GameProcessed)

GameProcessed is never used, so you shouldn’t include it as an argument.

function module:interact(player)
	game.ReplicatedStorage.DialogueRemote:FireClient(player, script.Parent.Dialogue)
end

Doesn’t it make more sense to only return the interact function rather than an otherwise unused table? Do you really need a ModuleScript for this if it is only used once? :sweat_smile:

local tweenGoals = {
				BackgroundColor3 = Color3.new(1, 1, 1)
			}
			
			tween.Tween(interactGui.Frame.interactKeyBG, 0.05, "Sine", "InOut", tweenGoals)
			wait(0.1)	
			local tweenGoals = {
				BackgroundColor3 = Color3.new(0.270588, 0.270588, 0.270588)
			}

Avoid tweening things on the server, particularly UI. UI should always be handled locally. Also, it makes no sense to define a variable for tweenGoals if it is only used once. You should instead define the goal argument inline to avoid unnecessary clutter. Additionally, it is better practice to employ Enums rather than the real values (ie Enum.EasingStyle.Sine).

Looks really good! Will you be using it in a game?

I’ve learned a lot of things since I made this and I definitely agree with everything you’ve said in this post.
Thanks for the feedack

Also I still had GameProcessed in there from earlier iterations

Doesn’t it make more sense to only return the interact function rather than an otherwise unused table? Do you really need a ModuleScript for this if it is only used once? :sweat_smile:

Yeah, at the time the only Idea I could come up with was using a module script