How can I cancel a function from outside the function?

Hi, everyone. I am making a sword fighting game. I am stuck on a problem but I can’t figure out how to solve it.
The way my sword should work: Once you start to block, for the first 2 seconds of the block, when hit by another player, the player which hit you should be stunned.

Right now if you hit block after blocking within 2 seconds from the first, the stun will end itself from the first spawn function loop. It should not do this. It should reset the stun back to the full 2 seconds.

Here is what I tried:

mouse.Button2Down:Connect(function()
	if attacking == false and equipped == true then
		blocking = true
		remoteEvent:FireServer("Block", true)
		spawn(function()
			remoteEvent:FireServer("Stun", true)
			wait(2)
			remoteEvent:FireServer("Stun", false)
		end)
		CurrentBlockAnimation = block_Animations[math.random(1, #block_Animations)]
		CurrentBlockAnimation:Play()
	end
end)

mouse.Button2Up:Connect(function()
	blocking = false
	remoteEvent:FireServer("Block", false)
	CurrentBlockAnimation:Stop()
end)

The remote events work fine and stuff. I just need a better way to create the stun and cancel it if block is let go of.

If for some reason you need to look at both of my scripts or you are just curious, here they are:
LOCAL: https://pastebin.com/Rx3cVRK6
SERVER: https://pastebin.com/c4wDfqKn
ENTIRE MODEL: https://www.roblox.com/library/4156389615/newStaff

local X = script.Parent.Changed:Connect(function()
	
end)

X:Disconnect()
2 Likes

As the title states, he wants to Disconnect the function from itself. I will recommend using a global variable in here.

Example



connection = script.Parent.Changed:Connect(function()
    --Stuff done in here that OP likes
    connection:Disconnect()
end)

The title stated he wanted to cancel it from outside the function, not from inside. however even if he wanted to disconnect from inside you would just remove the local.

Sorry, I missunderstood the original question.

Disconnecting functions doesn’t cancel them, all it does is prevent the connected function (RBXScriptConnection) from running again when the signal is fired. This is not proper cancellation: OP is looking to interrupt the function when it runs.

A function cannot be interrupted when it has a wait in it necessarily. The wait sleeps the running thread and a wait cannot be cancelled due to it performing thread sleeping on the scheduler internally.

cc @anon92559147 @GoldyyDev

4 Likes

I added:

local blockNum = 0

then put this into my block function:

	blockNum = blockNum + 1
	spawn(function()
		local BN = blockNum
		remoteEvent:FireServer("Stun", true)
		wait(2)
		if blockNum == BN then
			remoteEvent:FireServer("Stun", false)
		end
	end)

and it works perfectly.

Before I started writing 100% event-based code using my own schedulers as an alternative to wait(), I would have a “handle” that specifies what thread is currently in charge. Something like this:

local handle;
mouse.Button2Down:Connect(function()
	if attacking == false and equipped == true then
		-- Only run if not already running
		if not handle then
			local currentHandle = {}
			handle = currentHandle
			blocking = true
			remoteEvent:FireServer("Block", true)
			spawn(function()
				-- Cancel if handle changed
				if handle ~= currentHandle then return; end
				
				remoteEvent:FireServer("Stun", true)
				wait(2)
				
				-- Cancel if handle changed
				if handle ~= currentHandle then return; end
				
				remoteEvent:FireServer("Stun", false)
				
				-- Allow next attack
				handle = nil
			end)
			CurrentBlockAnimation = block_Animations[math.random(1, #block_Animations)]
			CurrentBlockAnimation:Play()
		end
	end
end)

mouse.Button2Up:Connect(function()
	if handle then
		handle = nil -- prevent stun from completing
		remoteEvent:FireServer("Stun", false)
	end
	
	blocking = false
	remoteEvent:FireServer("Block", false)
	CurrentBlockAnimation:Stop()
end)

Here’s what a simple event-based solution might look like:


local connection;
mouse.Button2Down:Connect(function()
	if attacking == false and equipped == true and not connection then
		blocking = true
		remoteEvent:FireServer("Block", true)
		
		remoteEvent:FireServer("Stun", true)
		
		local timestamp = tick() + 2
		connection = game:GetService("RunService").Heartbeat:Connect(function()
			if tick() > timestamp then -- waited 2 seconds
				connection:Disconnect()
				connection = nil
				
				remoteEvent:FireServer("Stun", false)
			end
		end)
		
		CurrentBlockAnimation = block_Animations[math.random(1, #block_Animations)]
		CurrentBlockAnimation:Play()
	end
end)

mouse.Button2Up:Connect(function()
	if connection then
		connection:Disconnect()
		connection = nil
		
		remoteEvent:FireServer("Stun", false)
	end
	blocking = false
	remoteEvent:FireServer("Block", false)
	CurrentBlockAnimation:Stop()
end)
1 Like

I have found Coroutine to be your best friend in here

Quick example with a doll function

local fun = coroutine.create(function()
	print('hi')
	wait(30) -- ahhhhh yield
	print('i stopped')	
end)

coroutine.resume(fun) -- start it

function stopFunctionCallBack()
	coroutine.yield(fun) --stop it
end
1 Like

You can only yield the running thread. If you didn’t want the part to run after the wait(30) some time later, you should set a flag to tell it to not run that part.

local flag
coroutine.wrap(function()
    --block
    wait(30)
    if not flag then
        --block
    end
end)()
1 Like

You know what, I actually just thought of something you can do. This is in regards to after Tomarty posted about event-based systems and I re-read the OP.

You’re using animations, yes? Make use of the AnimationTrack API then. Get rid of all that wait nonsense. Set up either keyframe names or keyframe markers. For keyframe names, use AnimationTrack.KeyframeReached. For markers, use AnimationTrack.GetMarkerReachedSignal.

For keyframe names, make three: Block (the very first keyframe, insert a blank one if there is no keyframe at 0:00 of the animation), StunStart (when true should be passed to the remote) and StunEnd (ditto, but false). For markers, make two events: Block and Stun. Have the first keyframe use the block marker and pass true as a parameter in the marker edit view, have the first stun keyframe use the stun marker pass true and the second one pass false. Regardless of what you choose, connect to AnimationTrack.Stopped and have it pass Block and false to the remote.

Easily a better solution than anything currently suggested. Event-based systems should always be your go-to rather than relying on wait which should scarcely be used in production code. If you need a wait, look to a Heartbeat-based wait from RunService (though I don’t know how to do that myself). You’d probably also be able to code cancellation from it or make a Timer API to further implement events.

Here’s some code samples of how to set this up with each item respectively.

NOTE NOTE NOTE: If you call LoadAnimation on your animations beforehand, don’t connect these events within your ButtonDown functions otherwise it’ll spawn up several connections and cause potential memory leaks or inflations. Connect events beforehand outside of the ButtonDown events, either from ModuleScripts containing the functions or just somewhere in the main script.

With named keyframes:

remoteEvent:FireServer("Block", true)
-- NOTE: If you ever use math.randomseed, consider using Random.new
-- For your random generator instead of math.randomseed + math.random
CurrentBlockAnimation = block_animations[math.random(1, #block_Animations)]
CurrentBlockAnimation.KeyframeReached:Connect(function (keyframeName)
    -- We don't use ternary otherwise it'd fire for every keyframe
    if keyframeName == "StunStart" then
        remoteEvent:FireServer("Stun", true)
    elseif keyframeName == "StunEnd" then
        remoteEvent:FireServer("Stun", false)
    end
end)
CurrentBlockAnimation.Stopped:Connect(function ()
    remoteEvent:FireServer("Block", false)
end)
CurrentBlockAnimation:Play()

With keyframe markers:

remoteEvent:FireServer("Block", true)
CurrentBlockAnimation = block_animations[math.random(1, #block_Animations)]
CurrentBlockAnimation:GetMarkerReachedSignal("Block"):Connect(function ()
    remoteEvent:FireServer("Block", true)
end)
CurrentBlockAnimation:GetMarkerReachedSignal("Stun"):Connect(function (boolean)
    remoteEvent:FireServer("Stun", boolean)
end)
CurrentBlockAnimation.Stopped:Connect(function ()
    remoteEvent:FireServer("Block", false)
end)
CurrentBlockAnimation:Play()

This should get rid of your worries of having to cancel functions entirely and you can have something both event-based and highly accurate for your case. I definitely recommend relying on events from the AnimationTrack keyframes rather than arbitrary waits (which in themselves can be desynchronised due to the extra time on wait spent searching for an open slot on the thread scheduler). :slightly_smiling_face:

2 Likes