Mining not being 100% accurate?

I’m finding if I move between a bunch of blocks, and then focus on one, that 1 block will destory quicker than it should.

-- Start mining the block
local function StartMining(input)
	local Character = Player.Character or Player.CharacterAdded:Wait()
	local Tool = Character:FindFirstChildWhichIsA("Tool")
	
	local Humanoid = Character:FindFirstChild("Humanoid")
	if not Humanoid then return end
	
	local Model = GetRay(input)
	if not Model then return end
	
	local BlockData = ItemData[Model.Name]
	if not BlockData then return end
	
	local MiningTime = BlockData.Time.Default
	CurrentObject = Model
	
	-- Check for tool
	if Tool then
		if ItemData[Tool.Name].Type == "Block" then return end -- Can't mine with Block
		
		if ItemData[Tool.Name].Type == "Tool" then
			if ItemData[Tool.Name].Category == BlockData.ToolCategory then
				-- Correct tool for this block type
				MiningTime = BlockData.Time[ItemData[Tool.Name].Material]
			end
		end
	end
	
	if Mining then return end
	
	Mining = true
	
	if BlockData.Type == "Block" then
		-- Blocks have crack
		CreateCrack(CurrentObject.Block)
		UpdateCracks(CurrentObject.Block, MiningTime)
	end
	
	while Mining do
		print("Start", MiningTime) -- Always prints 5
		wait(MiningTime)
		if not Mining then break end -- Stopped mining
		
		if CurrentObject ~= Model then return end -- Changed models between CurrentObject
		
		DestroyBlock:FireServer(CurrentObject)
		
		if CurrentObject then
			CurrentObject:Destroy()
			CurrentObject = nil
		end
	end
	
	Mining = false
end

local function InputBegan(input, GPE)
	if GPE then return end
	
	if input.UserInputType ~= Enum.UserInputType.MouseButton1 and input.UserInputType ~= Enum.UserInputType.Touch then return end
	
	StartMining(input)
end

local function InputEnded(input)
	if input.UserInputType ~= Enum.UserInputType.MouseButton1 and input.UserInputType ~= Enum.UserInputType.Touch then return end
	
	Mining = false
	
	if CurrentObject then
		DestroyCrack(CurrentObject.Block)
		CurrentObject = nil
	end
end

local function TargetChanged(input)
	if not Mining then return end
	
	if input.UserInputType ~= Enum.UserInputType.MouseMovement then return end
	
	local Model = GetRay(input)
	if not Model then return end
	
	if Model == CurrentObject then return end -- Same block
	
	if CurrentObject then -- Previous block exists
		DestroyCrack(CurrentObject.Block)
		
		CurrentObject = nil
	end
	
	Mining = false
	
	-- New object, restart
	StartMining(input)
end

UserInputService.InputBegan:Connect(InputBegan)
UserInputService.InputEnded:Connect(InputEnded)

-- Mouse moved
UserInputService.InputChanged:Connect(TargetChanged)

My goal is, to be basically identical in how minecrafts mining works. You have to hold click on a block to stay mining it. If you move off the block, it’ll start mining whatever block you moved onto.

Video of it working


However, sometimes it’ll break before the crack images have finished. It seems to be if I start mining it a bit, then stop, then come back, it’s like it picks back where it left off (which it shouldn’t)

1 Like

I didn’t see the DestroyCrack function, may I see it?

local function DestroyCrack(block)
	for _, v in pairs(block:GetChildren()) do
		if v:IsA("Texture") then
			v:Destroy()
		end
	end
end

Can you try to mine a block then stop mining it and then mine it again after 5 seconds? Also tell me if the bug happens.

Try replacing
wait(MiningTime)
with
local time--put this out of the function
time = time()
while time() - time < MiningTime do
wait()
if not Mining then
break
end
end

It’s probably acting like that because you restart mining the block before the mining time can end, so the script doesn’t even realize there was an input end. EDIT: Sorry about the spacing.

This might be an issue, when multiple players are mining. Instead I suggest destroying the block when the texture is the end texture or what you can do is store an IntValue inside the block, if the value is equal or bigger than X then you destroy the block.

X would be the hardness of the block, it would be an IntValue.

Your issue is a combination of

this
    while Mining do
		print("Start", MiningTime) -- Always prints 5
		wait(MiningTime)
		if not Mining then break end -- Stopped mining
		
		if CurrentObject ~= Model then return end -- Changed models between CurrentObject
		
		DestroyBlock:FireServer(CurrentObject)
		
		if CurrentObject then
			CurrentObject:Destroy()
			CurrentObject = nil
		end
	end
and this
local function InputEnded(input)
	if input.UserInputType ~= Enum.UserInputType.MouseButton1 and input.UserInputType ~= Enum.UserInputType.Touch then return end
	
	Mining = false
	
	if CurrentObject then
		DestroyCrack(CurrentObject.Block)
		CurrentObject = nil
	end
end

Releasing the mouse unsets Mining. Clicking the mouse again sets Mining. It also starts a 5 second timer, after which, if Mining is set, the block is destroyed. It doesn’t take into account which “process of mining a block” finished after those 5 seconds, so you can click, unclick, wait 4.9 seconds, click again, and the block destroys after 0.1 seconds. You could even exploit this to mine really fast.

To fix it, you need to either have a completely different approach, or somehow exit the while Mining do loop once the mouse is released / a “mining process” is stopped before it’s done.

Here's how you can change the loop to work (maybe, I haven't tested it):
	local stoppedMining = false
	while Mining do
		print("Start", MiningTime) -- Always prints 5
		local waitTimeRemaining = MiningTime
		while true do
			local timeElapsed = wait()
			waitTimeRemaining -= timeElapsed()
			if not Mining then
				stoppedMining = true
				break
			end
		end

		if (not Mining) or stoppedMining then break end -- Stopped mining
		
		if CurrentObject ~= Model then return end -- Changed models between CurrentObject
		
		DestroyBlock:FireServer(CurrentObject)
		
		if CurrentObject then
			CurrentObject:Destroy()
			CurrentObject = nil
		end
	end

Instead of “one big 5 second wait”, it’s “lots of little waits” that decrement a 5 second timer. Each time after waiting around 1/30th of a second, it checks if Mining is false, and takes that to mean that the mining process was stopped.

This is far from perfect, because it might be possible for a player to release the mouse, move it to another block, and click again within the 1/30th of a second time-frame. You could use RenderStepped:Wait() instead of wait to get around 1/60th of a second, but this can be much slower depending on the Player’s device.

1 Like

How I’d redesign your code to not have this problem:

Use an event connection to update the 5 second timer to see if it’s finished. That way, the timer can simply be stopped by disconnecting the connection. That can look something like this:

local miningRenderSteppedConnection

function finishMining()
	--destroy the block, etc.
end

function startMining()
	local miningTimeLeft = miningTime

    miningRenderSteppedConnection = RunS.RenderStepped:Connect(function(dt)
    	miningTimeLeft -= dt

    	if mininingTimeLeft <= 0 then
    		finishMining()
    		stopMining()
    	end
    end)
end

function stopMining()
    if miningRenderSteppedConnection then
	    miningRenderSteppedConnection:Disconnect()
	    miningRenderSteppedConnection = nil
    end
end

You can abstract this idea of timers out into a function that creates timers that can be stopped, and which call a function when they run out:

function createTimer(length, finishedCallback)
	local timeLeft = length
	local connection

	connection = RunS.RenderStepped:Connect(function(dt)
		timeLeft -= dt
		if timeLeft <= 0 then
			connection:Disconnect()
			finishedCallback()
		end
	end)

	return connection
end

If you choose to do that, the code for starting and stopping the mining process becomes super simple:

local miningTimer

function createTimer(length, finishedCallback)
	local timeLeft = length
	local connection

	connection = RunS.RenderStepped:Connect(function(dt)
		timeLeft -= dt
		if timeLeft <= 0 then
			connection:Disconnect()
			finishedCallback()
		end
	end)

	return connection
end

function finishMining()
	--destroy the block, etc.
end

function startMining()
	miningTimer = createTimer(miningTime, function()
		finishMining()
		stopMining()
	end)
end

function stopMining()
	if miningTimer then
		miningTimer:Disconnect()
		miningTimer = nil
	end
end

EDIT: If you want to do something every time the timer “updates”, you can add another parameter to the createTimer function called “updateCallback”, and call that every time timeLeft variable gets decremented. You could e.g. have a updateCallback that updates the cracks for the currently mined block.

A little confused on how to organise all this? Not sure what parts of my code to keep/remove

function FinishMining()
	print("Destroy")
end

-- Start mining the block
local function StartMining(input)
	local Character = Player.Character or Player.CharacterAdded:Wait()
	local Tool = Character:FindFirstChildWhichIsA("Tool")
	
	local Humanoid = Character:FindFirstChild("Humanoid")
	if not Humanoid then return end
	
	local Model = GetRay(input)
	if not Model then return end
	
	local BlockData = ItemData[Model.Name]
	if not BlockData then return end
	
	local MiningTime = BlockData.Time.Default
	CurrentObject = Model
	
	-- Check for tool
	if Tool then
		if ItemData[Tool.Name].Type == "Block" then return end -- Can't mine with Block
		
		if ItemData[Tool.Name].Type == "Tool" then
			if ItemData[Tool.Name].Category == BlockData.ToolCategory then
				-- Correct tool for this block type
				MiningTime = BlockData.Time[ItemData[Tool.Name].Material]
			end
		end
	else
		-- Empty handed, play punch
		
		-- Play punch animation
		-- Punch sound
	end
	
	if Mining then return end
	
	Mining = true
	
	if BlockData.Type == "Block" then
		-- Blocks have crack
		CreateCrack(CurrentObject.Block)
		UpdateCracks(CurrentObject.Block, MiningTime)
	end
	
	local miningTimeLeft = MiningTime

	miningRenderSteppedConnection = game:GetService("RunService").RenderStepped:Connect(function(dt)
		miningTimeLeft -= dt
		
		if miningTimeLeft <= 0 then
			FinishMining()
			StopMining()
		end
	end)
	
	Mining = false
end

function StopMining()
	if miningRenderSteppedConnection then
		miningRenderSteppedConnection:Disconnect()
		miningRenderSteppedConnection = nil
	end
end

local function InputBegan(input, GPE)
	if GPE then return end
	
	if input.UserInputType ~= Enum.UserInputType.MouseButton1 and input.UserInputType ~= Enum.UserInputType.Touch then return end
	
	StartMining(input)
end

local function InputEnded(input)
	if input.UserInputType ~= Enum.UserInputType.MouseButton1 and input.UserInputType ~= Enum.UserInputType.Touch then return end
	
	Mining = false
	
	if CurrentObject then
		DestroyCrack(CurrentObject.Block)
		CurrentObject = nil
	end
end

local function TargetChanged(input)
	if not Mining then return end
	
	if input.UserInputType ~= Enum.UserInputType.MouseMovement then return end
	
	local Model = GetRay(input)
	if not Model then return end
	
	if Model == CurrentObject then return end -- Same block
	
	if CurrentObject then -- Previous block exists
		DestroyCrack(CurrentObject.Block)
		
		CurrentObject = nil
	end
	
	Mining = false
	
	-- New object, restart
	StartMining(input)
end

UserInputService.InputBegan:Connect(InputBegan)
UserInputService.InputEnded:Connect(InputEnded)

-- Mouse moved
UserInputService.InputChanged:Connect(TargetChanged)

Destroy print gets spammed

You wouldn’t use the Mining variable to control what happens. Instead of setting Mining to false, you’d call StopMining. E.g.:

local function InputEnded(input)
	if input.UserInputType ~= Enum.UserInputType.MouseButton1 and input.UserInputType ~= Enum.UserInputType.Touch then return end
	
	StopMining()
	
	if CurrentObject then
		DestroyCrack(CurrentObject.Block)
		CurrentObject = nil
	end
end

I don’t get any update on cracks, and when my mouse moves off the block, the crack stays

-- Crack stuff
local function UpdateCracks(block, waitTime)
	print("Update")
	coroutine.wrap(function()
		for i = 1, #Cracks do
			if not Mining then return end -- Stop loop when not mining
			
			for _, v in pairs(block:GetChildren()) do
				if v:IsA("Texture") then
					v.Texture = "rbxassetid://" .. Cracks[i]
				end
			end
			
			wait(waitTime / #Cracks)
		end
	end)()
end

EDIT Realised I Still had Mining around the place. After removing all reference to it, it nows starts mining just when I hover on a block, and the Destroy function gets spammed

I tried modifying your original code to work the way I suggested, with some modifications that makes it so there’s fewer globals and IMO it’s easier to follow the logic of what happens when.


local miningRenderSteppedConnection, miningInputChangedConnection

function finishMining(TargetBlock)
	DestroyBlock:FireServer(TargetBlock)
	
	if TargetBlock then
		TargetBlock:Destroy()
		TargetBlock = nil
	end
end

local function UpdateCracks(block, miningProgress)
	local crackTextureNum = math.ceil( miningProgress / #Cracks )
	local crackProgressTexture = "rbxassetid://" .. Cracks[crackTextureNum]
	for _, v in pairs(block:GetChildren()) do
		if v:IsA("Texture") then
			v.Texture = crackProgressTexture
		end
	end
end

function stopMining(TargetBlock)
	DestroyCrack(TargetBlock.Block)

    if miningRenderSteppedConnection then
	    miningRenderSteppedConnection:Disconnect()
	    miningRenderSteppedConnection = nil
    end

    if miningInputChangedConnection then
		miningInputChangedConnection:Disconnect()
	    miningInputChangedConnection = nil
    end
end

function canStartMining()
	local Character = Player.Character or Player.CharacterAdded:Wait()
	if not Character then return false end

	local Humanoid = Character:FindFirstChild("Humanoid")
	if not Humanoid then return false end
	
	local TargetBlock = GetRay(input)
	if not TargetBlock then return false end

	return true
end

-- Start mining the block
local function startMining(input)
	if not canStartMining() then return end

	local Tool = Character:FindFirstChildWhichIsA("Tool")	
	local TargetBlock = GetRay(input)
	local BlockData = ItemData[TargetBlock.Name]
	local MiningTime = BlockData.Time.Default
	local miningTimeLeft = MiningTime
	
	-- See if the equipped tool should modify MiningTime
	if Tool then
		if ItemData[Tool.Name].Type == "Block" then return end -- Can't mine with Block
		
		if ItemData[Tool.Name].Type == "Tool" then
			if ItemData[Tool.Name].Category == BlockData.ToolCategory then
				-- Correct tool for this block type
				MiningTime = BlockData.Time[ItemData[Tool.Name].Material]
			end
		end
	end

	-- Blocks have cracks, other types don't.
	if BlockData.Type == "Block" then
		CreateCrack(TargetBlock.Block)
	end

	miningRenderSteppedConnection = RunS.RenderStepped:Connect(function(dt)
    	miningTimeLeft -= dt

    	UpdateCracks(TargetBlock.Block, miningTimeLeft)

    	if mininingTimeLeft <= 0 then
    		finishMining(TargetBlock)
    		stopMining(TargetBlock)
    	end
    end)

    miningInputChangedConnection = input.Changed:Connect(function(...)
    	if input.UserInputState == Enum.UserInputState.End then
    		stopMining()
    	else
    		local newTargetBlock = GetRay(input)
    		if newTargetBlock ~= TargetBlock then
    			stopMining(TargetBlock)
    			
    			if newTargetBlock then
    				startMining(input)
    			end
    		end
    	end
    end)

end

UserInputService.InputBegan:Connect(function(input, gameProcessedEvent)
	if gameProcessedEvent then return end

	if input.UserInputType == Enum.UserInputType.MouseButton1 then
		startMining()
	end
end)