Multiplayer Server Script Help

I am creating my first game, a mining simulator. I am trying to create a system like Treasure Hunt Simulator/Mining Simulator where multiple people can mine a single block at the same time. Everything works great so far in single player, but I’m running into issues with the server script that manages mining when multiple players try to mine.

It seems like the issue is that when a second player activates this script before a first player is finished running it, the variables values get messed up and the results don’t make sense. For example, this script has a debouncer variable. If a second player starts mining right after the first player, then the second player is stopped by the debounce and unable to mine.

My question is: how can I design a server-side mining manager that works for multiplayer?

When I wrote this script, I thought that a new “instance” of the script would be created and run every time a new client fired a remote event that triggered the server script (kind of like a local script that a hacker can’t access). Therefore, variables would not be an issue if the script was run multiple times by multiple clients. It seems like this is not how this works. There aren’t too many dev hub articles that explain server scripts well (Client-Server Runtime | Documentation - Roblox Creator Hub)…

Thanks!!

Server Script
local ReplicatedStorage = game:GetService("ReplicatedStorage")

--Cache Module
local PartCache = require(ReplicatedStorage.Modules:WaitForChild("PartCache"))


--Get Mining Remote Events
repeat wait() until ReplicatedStorage:FindFirstChild("ActivateMining")
local ActivateMining = ReplicatedStorage.ActivateMining
repeat wait() until ReplicatedStorage:FindFirstChild("DeactivateMining")
local DeactivateMining = ReplicatedStorage.DeactivateMining
repeat wait() until ReplicatedStorage:FindFirstChild("DestroyBlock")
local DestroyBlock = ReplicatedStorage.DestroyBlock
repeat wait() until ReplicatedStorage:FindFirstChild("MiningTimerStart")
local MiningTimerStart = ReplicatedStorage.MiningTimerStart
repeat wait() until ReplicatedStorage:FindFirstChild("MiningTimerStop")
local MiningTimerStop = ReplicatedStorage.MiningTimerStop
repeat wait() until ReplicatedStorage:FindFirstChild("MiningDamage")
local MiningDamage = ReplicatedStorage.MiningDamage

--Block Target Variables
local MiningTarget = nil
local Health
local BaseMineSpeed

--Player Tool Variables
local ActivePlayer
local ToolSpeedMultiplier
local ToolDamage

--Debouncer
local enabled = true
local enabled2 = true
local waitbreak = false

local function CoreMining(Player)
	local player = Player
	local dust = player.leaderstats.Dust

	while MiningTarget ~= nil do

		--Mining time
		local MineTime = BaseMineSpeed.Value/ToolSpeedMultiplier.Value

		if MiningTarget ~= nil then
			MiningTimerStart:FireClient(player, MineTime)
			print("Mining Start")
			wait(MineTime)
			print("Mine Wait Complete")
		end
		

		--Double Check Player has not let go of button
		if MiningTarget == nil then
			enabled = true
			break
		else
			--Do Dmg to Block
			Health.Value = Health.Value - ToolDamage.Value

			--Give player dust based on damage done
			dust.Value = dust.Value + ToolDamage.Value
			MiningDamage:FireClient(player, ToolDamage.Value)
			enabled = true

			if Health.Value <= 0 then
				--Destroy Block
				DestroyBlock:Fire(MiningTarget.Position)
				if MiningTarget.Parent.Name == "MiningBlocks" then
					MiningTarget:Destroy()
				elseif MiningTarget.Parent.Name == "Generated" then
					PartCache:ReturnPart(_G.SandCache)
				end


				--Delete Target Block Mine Variables
				MiningTarget = nil
				Health = nil
				BaseMineSpeed = nil

				--Delete Tool Variables
				ActivePlayer = nil
				ToolSpeedMultiplier = nil
				ToolDamage = nil


				break
			end	
		end		

	end
end


--Activation Status

local function ActivateMiningBool(Player, Target, Damage, SpeedMultiplier)
	
	if not enabled then
		print("Can't Start, Not Enabled")
		print(Player)
		return 
	end

	enabled = false

	--Set Target Block Mine Variables
	print(Target)
	
	MiningTarget = Target
	Health = Target.Health
	BaseMineSpeed = Target.BaseMineSpeed

	--Set Tool Variables
	ActivePlayer = Player
	ToolSpeedMultiplier = SpeedMultiplier
	ToolDamage = Damage

	CoreMining(ActivePlayer)

	--Debouncer
	enabled = true

	return 

end



local function DeactivateMiningBool(Player)
	
	
	if not enabled2 then
		return
	end

	enabled2 = false

	--Stop Gui
	MiningTimerStop:FireClient(Player)

	--Delete Target Block Mine Variables
	MiningTarget = nil
	Health = nil
	BaseMineSpeed = nil

	--Delete Tool Variables
	ActivePlayer = nil
	ToolSpeedMultiplier = nil
	ToolDamage = nil

	enabled2 = true

end

--Fires when mouse button pushed
ActivateMining.OnServerEvent:Connect(ActivateMiningBool)

--FIres when moust button released
DeactivateMining.OnServerEvent:Connect(DeactivateMiningBool)
1 Like

Can I see the local script for the one player mining?

Easy way to do it would be to just set the enabled enabled2 and waitbreak as tables instead and use a player or userid as the index then read the value that way.

i.e.,

...
--Debouncer
local enabled = {}; --true
local enabled2 = {}; --true
local waitbreak = {}; --false

...

local function DeactivateMiningBool(Player)
	
	
	if not enabled2[Player] then
		return
	end

	enabled2[Player] = false

	--Stop Gui
	MiningTimerStop:FireClient(Player)

	--Delete Target Block Mine Variables
	MiningTarget = nil
	Health = nil
	BaseMineSpeed = nil

	--Delete Tool Variables
	ActivePlayer = nil
	ToolSpeedMultiplier = nil
	ToolDamage = nil

	enabled2[Player] = true

end

...

game:GetService("Players").PlayerAdded:Connect(function(player)
  enabled[player] = true;
  enabled2[player] = true;
  waitbreak[player] = false;
end);
1 Like

Sure, this is the local script. It checks to see 1. what the player is hovering their mouse over, 2. if the player is in range of the block and 3. if MB1 is down.

Local Script
--Detect User Input Type Module
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlayerInputModule = require(ReplicatedStorage:WaitForChild("PlayerInputModule"))
local currentPlayerInput, inputEnum = PlayerInputModule.getInputType()

--Only run this script if player input is mouse/keyboard--
if currentPlayerInput == "Keyboard/Mouse" then
	--Tool & its stats
	local tool = script.Parent
	local TOOL_RANGE = tool.Range.Value
	repeat wait() until tool:FindFirstChild("Damage")
	local Damage = tool.Damage
	repeat wait() until tool:FindFirstChild("SpeedMultiplier")
	local SpeedMultiplier = tool.SpeedMultiplier
	
	
	--Equipped/Unequipped Tools
	--Tool Equip Status
	local equipped = false --Mah first line of code =]

	--Equip Functions
	local function equipTool()
		equipped = true
	end
	local function unequipTool()
		equipped = false
	end

	tool.Equipped:connect(equipTool)
	tool.Unequipped:connect(unequipTool)

	--Activation Events
	repeat wait() until ReplicatedStorage:FindFirstChild("ActivateMining")
	local ActivateMining = ReplicatedStorage.ActivateMining
	repeat wait() until ReplicatedStorage:FindFirstChild("DeactivateMining")
	local DeactivateMining = ReplicatedStorage.DeactivateMining

	--MiningBlocks Folder --
	local MiningBlocksFolder = workspace.MiningBlocks

	--Get Player Location for Raycast Origin
	local player = game.Players.LocalPlayer

	--player.CharacterAdded:Wait()
	local character = player.Character
	character:WaitForChild("Head")
	local playerHead = character.Head


	--Get User Input Service
	local UserInputService = game:GetService("UserInputService")

	--Get Current Camera
	local Camera = game.Workspace.CurrentCamera

	--Get Player Mouse
	local mouse = player:GetMouse() 
	local MouseButtonDown = false
	local MouseMoveConnection


	-----Selection Ray-----
	local function GetMouseLocation()
		local Position = UserInputService:GetMouseLocation()
		return Camera:ViewportPointToRay(Position.X, Position.Y)
	end

	local function SelectionRay()
		--Establish raycast params for raycast
		local raycastParams = RaycastParams.new()
		raycastParams.FilterType = Enum.RaycastFilterType.Whitelist
		raycastParams.FilterDescendantsInstances = {workspace.MiningBlocks} --Only mining blocks can be hit
		raycastParams.IgnoreWater = true


		local firstRay = GetMouseLocation() --Ray from camera to mouse
		local firstRayResult = workspace:Raycast(firstRay.Origin, firstRay.Direction * 500, raycastParams)
		if firstRayResult then
			firstRayResult = firstRayResult.Instance
		end
		return firstRayResult 
	end

	-----Minable Check Ray-----
	local function MiningCheckRay()
		--Get first ray result for check ray direction
		local firstRayResult = SelectionRay()

		if firstRayResult then
			--New CFrame pointing to mouse location from head
			local newCF = CFrame.new(playerHead.Position, firstRayResult.Position) 

			--Establish raycast params for second raycast
			local raycastParams = RaycastParams.new()
			raycastParams.FilterType = Enum.RaycastFilterType.Whitelist
			raycastParams.FilterDescendantsInstances = {workspace.MiningBlocks} --Only mining blocks can be hit
			raycastParams.IgnoreWater = true

			--Second ray from player head to mouse position
			local secondRay = workspace:Raycast(playerHead.Position, newCF.LookVector * TOOL_RANGE, raycastParams)

			if secondRay then
				local mineBlock = secondRay.Instance
				return mineBlock
			else
				return
			end
		else 
			return
		end
	end


	--Selection Box Creator
	local sb = Instance.new("SelectionBox", game.Workspace.Camera) -- Creates a new Selection Box
	sb.LineThickness = 0.05 -- Customizable Thickness
	sb.SurfaceTransparency = 1 -- Customizable Transparency	
	sb.Parent = player.PlayerGui
	
	local function SelectionBoxChecker()
		--Create selection box object
		local gui = player.PlayerGui:WaitForChild("HoverGui") -- Waits for the "HoverGui" in StarterGui(optional)

		--Call both ray checks
		local MouseSelection = SelectionRay()
		local MineSelection = MiningCheckRay()

		--Create selection box
		--Case if black is minable
		if MouseSelection == MineSelection and MouseSelection ~= nil and MineSelection ~= nil then
			sb.Adornee = MouseSelection
			print("adornee set:", sb.Adornee)
			sb.Color3 = Color3.fromRGB(0, 135, 0)

			gui.Frame.Visible = true -- Makes the Frame from the Gui(Line-14) Visible(optional)
			gui.Frame.TextLabel.Text = "Minable"
			
			--Listen for block destroy
			local destroyed = false
			sb.Adornee.AncestryChanged:connect(function()
				destroyed = true
				sb.Adornee = nil
			end)
			
			
			--Mining Activation
			if MouseButtonDown == true and equipped == true then
				ActivateMining:FireServer(MouseSelection, Damage, SpeedMultiplier)
				MouseMoveConnection:Disconnect()
				repeat
					wait()
				until (MouseButtonDown == false or equipped == false or destroyed == true) 
				MouseMoveConnection = nil
			end
			
			
			--Mining Deactivation
			if ((MouseButtonDown == false or equipped == false) and MouseMoveConnection == nil) then
				MouseMoveConnection = mouse.Move:Connect(SelectionBoxChecker)
				DeactivateMining:FireServer()
				sb.Adornee = nil
			elseif destroyed == true then
				MouseMoveConnection = mouse.Move:Connect(SelectionBoxChecker)
				DeactivateMining:FireServer()
				sb.Adornee = nil
				SelectionBoxChecker()
			end
			
		--Case if block is obstructed
		elseif MouseSelection ~= nil and MineSelection ~= nil then
			sb.Adornee = MouseSelection
			sb.Color3 = Color3.fromRGB(170, 0, 0)

			gui.Frame.Visible = true -- Makes the Frame from the Gui(Line-14) Visible(optional)
			gui.Frame.TextLabel.Text = "Obstructed"

		--Case if block is out of range of player
		elseif MouseSelection ~= nil and MineSelection == nil then
			sb.Adornee = MouseSelection
			sb.Color3 = Color3.fromRGB(170, 0, 0)

			gui.Frame.Visible = true -- Makes the Frame from the Gui(Line-14) Visible(optional)
			gui.Frame.TextLabel.Text = "Out of Range"


		else
			sb.Adornee = nil -- Makes the Adornee of the Selection Box to nothing
			gui.Frame.Visible = false -- Makes the Frame from the Gui not Visible(optional)

		end

		--Clear selection box and gui on next move event!
		mouse.Move:Connect(function()
			gui.Frame.Visible = false -- Makes the Frame from the Gui not Visible(optional)
		end
		)



	end

	--Mouse Move
	MouseMoveConnection = mouse.Move:Connect(SelectionBoxChecker)

	-----Mouse Button Up/Down---
	UserInputService.InputBegan:Connect(function(input)
		local inputType = input.UserInputType
		if inputType == Enum.UserInputType.MouseButton1 then
			MouseButtonDown = true
			SelectionBoxChecker()
		end
	end)

	UserInputService.InputEnded:Connect(function(input)
		local inputType = input.UserInputType
		if inputType == Enum.UserInputType.MouseButton1 then
			MouseButtonDown = false
		end
	end)

end

This makes sense, thanks a bunch! I’ll try it out today.

It worked! Thanks again!!! Should be able to use this structure for all of my other server scripts too.

1 Like