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)
Easy way to do it would be to just set the enabledenabled2 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);
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