Task.Spawn in a module script cancels when a script requiring it is destroyed

I have a tool in my game, When activated, Its supposed to get a module script it required, and start the “Start” function, This function contains a Task.Spawn, with some task.waits.
Module script in question:

function modulescript:Start()
	task.spawn(function()
		task.wait(timeamount)
		--do some stuff
		task.wait(timeamountsecond)
		--do more stuff
	end)
end

(this isnt what the actual script says, its just to give you an idea of what its like.)
Anyways, The reason the waits are there is because i want the module script to handle all the clean up, because if the player with the tool leaves in the middle of it running, the waits that used to be in the tools would obviously stop running, therefore the stuff from the module is left over.

So the issue now, is if the player with the tool leaves (basically just if the script stops running), the waits in the module script will stop, For whatever reason.
tool script:

local modulescript = require(pathtomodule)

script.Parent.Parent.Activated:Connect(function()
	modulescript:Start()
end)

(also not the real tool script)

Any help is appreciated!

2 Likes

This is a problem related to how threads are handled. I’ll try to break it down.

Threads of code from events to task.spawn() to loose code in a script that just runs are all “attached” to the script that created them. When a script is destroyed, so are all the threads attached to it, meaning threads that aren’t done running their code will appear to stop.

Q: What about threads created in ModuleScripts like in my modulescript:Start()?
A: Those threads are attached to the server/client script that called the function in the module, NOT the module housing the code. Thread management isn’t any ModuleScript’s problem or purpose.

Now, how about a solution? Well, I have a simple workaround. Just make a BindableEvent, parent it to ServerScriptService, insert a script into the BindableEvent, and change the code within to the following:

script.Parent.Event:Connect(function(fnc) fnc() end)

Boom! You now have in independent thread spawner. What this does is allow scripts to pass functions to a completely separate script, which “detaches” threads from the script that passed along the functions. Here’s a simple demo (script is also parented to ServerScriptService):

local SpawnThread = script.Parent.SpawnIndependentThread --// SpawnIndependentThread is what I name the BindableEvent

SpawnThread:Fire(function()
	script:Destroy()
	print("Well, that was easy.") --// This will run
end)
4 Likes

Ah, okay.

Im a bit confused on how i should incorporate this into my script however.
Basically, let me explain what my script actually does.

Its a hitbox module, the reason for the waits is so it’ll automatically clean up hitboxes, And im not fully sure if having a server script with a bindable event would be very great for a game about combat, as tons of hitboxes will be created all the time. (im not sure if it would lag actually.)
i appreciate you taking the time to write an entire message to my problem.
I dont intend to be annoying, but im a little lost right now.
Could you explain how i could set this up with my current set up?

Also, the time it takes to clean up a hitbox is from a value that is sent to the module when a script creates a hitbox. How should i get this value from the server script?

Well, I’m a little confused about your setup. Could you tell me more about how it’s structured, give me details about your game’s weapons, and explain why the cleanup from the tool(s) eventually being destroyed wouldn’t suffice?

Sorry, im pretty bad at explaining things.

The scripts inside (if this is what you mean) require the module at the top of the script,
and then start the “Start” function, before that, they change some parameters to make the hitbox different, These parameters include the Clean up time.
They worked fine, but i was kind of annoyed that players could just leave right as the hitbox is made, Sure, it wouldn’t hurt anyone since the stuff for hurting is in the tool, but still, the hitbox isnt removed.

Because the hitbox is left over, The script waits after being activated, and then stops the hitbox, If its destroyed in the middle of the wait, the hitbox stays there.

Sorry if i dont understand properly.

I keep thinking about it and I’m bothered by how you say you’re constantly creating hitboxes instead of just creating reusable ones that can be turned on and off (like hitboxes made with RaycastHitbox or Part hitboxes with a CanDamage boolean). As far as I know, the hitboxes should be destroyed with the weapon unless they aren’t a descendant of the tool (e.g. hitboxes parented to the player’s limbs for punching and kicking attacks). If you can tell me which hitbox method you’re using and if the hitboxes are descendants of the tool, then that would help immensely.

That aside, here’s some more info about using that bindable. You wouldn’t have to put all of whatever code into the function you’d pass in. I would imagine you’d only have to pass in the cleanup operation.

--// Server script
local modulescript = require(pathotmodule)

script.Parent.Parent.Activated:Connect(function()
    modulescript:Start()
end)

--// Module script
local modulescript = {}
local SpawnThread = PathToBindableEvent

function modulescript:Start()
    
    --// Initial process(es) and hitbox management whatever here
    
    --// The cleanup operation should be in a "detached" thread, that way the cleanup will definitely happen
    SpawnThread:Fire(function()
        wait(<time>) --// Put the delay here
        --// Cleanup operation here
    end)

end
1 Like

Now that i think about it, Considering the way my hitbox system is set up, i could just use 1 hitbox and activate and deactivate when i need it, Or change the size, or just different parameters.

Also, i have used raycast hitbox before, but its not what im looking for.

spatial queries, The box that visualizes the hitbox is in Terrain, but i have them turned off.

Im honestly very new to making hitboxes and using spatial queries, so i dont know if they can cause problems if not removed, or if they can even be removed.

I will try the code out, Thank you for your help!

Alright, I tested it (apologies it took so long)
It didn’t seem to work, To be honest, I think destroying/disabling the hitbox via the tool itself would be just fine, But im not sure how to set that up.
Does Part.Destroying() Fire just before its destroyed? if so, would i have enough time to disable the hitbox from there?
Also, i was thinking about what you said about creating hitboxes everytime, So would this kind of set up be fine?

Create hitbox at the start of the script.

when activated, Depending on what kind of move it is, Change parameters of the
hitbox, Wait a few seconds, and disable the hitbox.
On destroying, Cancel the hitbox.
On unequipped, stop the hitbox.

Would this set up be okay? or should i change something? (asking if theres like a better way, not if it’ll work or not)

okay, so i just tested it out, and im completely stuck now, the Destroying function doesnt happen before the script is destroyed, so i just cant delete the hitbox along with the tool.
I have absolutely no idea what to do now.

Well, I’m lost on what to do as well. I’ve never heard of people using spatial queries for hitboxes. We both seem to be confused about what’s going on, so let’s step back to getting details on the weapon, that way I’m not shooting at targets in the dark. I would like you to thoroughly describe the attack and how it’s meant to function. Perhaps you could also get a video of your current progress and/or what you have in mind?

Weird. I thought spatial queires were normal for hitboxes.

Im using this module for every melee weapon in my game (combat game)
For the most part, All it does is when the tool is activated, create the hitbox via module, change parameters, and start it, The way the system works is okay, My only issue is destroying the hitbox when the script/tool is destroyed.

The hitboxes damage and knockback players and NPCs.


As seen in here, the hitboxes work perfectly, Ive tested with some friends, and havent gotten any issues, apart from the hitboxes not going away if you leave/respawn.

Full in detail hitbox system:
Upon activation, Module is told to run a function, The function takes parameters from the hitbox (that the module created, but the script changed) These parameters include Size, CFrame, But the most important ones, SpawnDelayTime and AutomaticDestroyTime, Once function for starting is called, A task.Spawn is created, And waits the SpawnDelayTime amount, starts up hitbox, Waits AutomaticDestroyTime, and removes the hitbox.

Apologies if this isnt really enough information.
I thought it was normal for Spatial Queries to be used as hitboxes, If they are, Then how do other people remove them when the script is destroyed?

O H so THAT’S how your hitboxes work. Okay yeah I have seen hitboxes like that a few times and I misunderstood what you were doing, so my apologies. I thought all along the hitboxes were something more like what I usually see, which run on either RaycastHitbox or .Touched, but now I understand.

Well, when it’s normal depends on what the weapon is and how the developer wants it to behave, but if we’re asking what is the most used hitbox method across all of Roblox, then the answer is not spatial queries.

.Touched and RaycastHitbox hitboxes (the latter being superior) are easier to script, thus making them more appealing over more complicated solutions. On the weapon behavior side of things, they’re ideal for when precision is desired, like for weapons with movesets players are meant to learn and master over time. Spatial queries are better suited for things that create hazardous regions, unlike sword blade swings and gun bullets, but very much like smash attacks and explosions. You’re using spatial queries to cover a sizeable region in front of the player, which is a perfectly valid and definitely not unheard of way to use them. Let me make it perfectly clear that I am not trying to persuade you to use a different method. I am curious what about it makes you use it instead RaycastHitbox though.

Alrighty enough rambling. I ended up writing an entire damn module while trying to figure out how to handle deleting the hitbox, which I concluded is shoving everything into a “detached” thread and constantly checking for things like the existence of the player, their character, etc. I’m certain you’ll have to modify the code below to work with your game’s specifics, but I hope this will serve as a basis.

--// MODULE SCRIPT
--// THIS USES THE INDEPENDENT THREAD SPAWNER
local SQHitbox = {}
local SpawnThread = game.ServerScriptService.SpawnIndependentThread
local wait = task.wait

type SpatialQueryHitbox = {
	SetDamage: (num:number) -> (),
	CreateHitbox: (CF:CFrame,Size:Vector3,SpawnDelayTime:number,AutomaticDestroyTime:number) -> ()
}

local HitboxDump = Instance.new("Folder")
HitboxDump.Name = "SQHitboxDump"
HitboxDump.Parent = workspace

function SQHitbox.New(Player:Player,Weapon:Tool): SpatialQueryHitbox
	local PlrCharacter:Model = Player.Character
	local DamageToInflict = 0
	return {
		SetDamage = function(num)
			DamageToInflict = num
		end,
		CreateHitbox = function(CF,Size,SpawnDelayTime,AutomaticDestroyTime)
			SpawnThread:Fire(function()
				if PlrCharacter.Humanoid.Health <= 0 then return end
				
				--// Get the player's tool so the script can see if it gets unequipped
				--// Make sure it's the weapon tho
				local EquippedTool = PlrCharacter:FindFirstChildOfClass("Tool")
				if EquippedTool ~= Weapon then return end
				local UnequippedConnection = nil
				UnequippedConnection = Weapon.Unequipped:Connect(function()
					UnequippedConnection:Disconnect()
					UnequippedConnection = nil
				end)

				--// Wait
				if SpawnDelayTime and SpawnDelayTime ~= 0 then
					wait(SpawnDelayTime)
					if not (Player and PlrCharacter and UnequippedConnection) then return end
				end

				--// Watch for the player's death
				local HealthMonitorConnection = nil
				HealthMonitorConnection = PlrCharacter.Humanoid:GetPropertyChangedSignal("Health"):Connect(function()
					if PlrCharacter.Humanoid.Health <= 0 then
						HealthMonitorConnection:Disconnect()
						HealthMonitorConnection = nil
					end
				end)

				--// Create the hitbox part
				local HitboxPart = Instance.new("Part")
				HitboxPart.Name = "SQHitbox"
				HitboxPart.CFrame = CF
				HitboxPart.Size = Size
				HitboxPart.Anchored = true
				HitboxPart.Transparency = 0.5
				HitboxPart.Color = Color3.new(1,0,0)
				HitboxPart.CastShadow = false
				HitboxPart.CanCollide = false
				HitboxPart.CanQuery = false
				HitboxPart.CanTouch = false
				HitboxPart.Parent = HitboxDump

				--// Check for things hitting the hitbox
				local OverParams = OverlapParams.new()
				OverParams.FilterType = Enum.RaycastFilterType.Exclude
				OverParams.FilterDescendantsInstances = {HitboxPart,PlrCharacter}
				local TimeElapsed = 0
				while TimeElapsed < AutomaticDestroyTime and UnequippedConnection and HealthMonitorConnection and Player and PlrCharacter do
					local HitCharacters = {}
					for i,v in pairs(workspace:GetPartsInPart(HitboxPart,OverParams)) do
						if v.Parent:FindFirstChildOfClass("Humanoid") and not table.find(HitCharacters,v.Parent) then
							table.insert(HitCharacters,v.Parent)
							v.Parent:FindFirstChildOfClass("Humanoid"):TakeDamage(DamageToInflict)
						end
					end
					OverParams:AddToFilter(HitCharacters)
					TimeElapsed += wait()
				end
				
				--// Cleanup
				if HitboxPart then
					HitboxPart:Destroy()
				end
				if UnequippedConnection then 
					UnequippedConnection:Disconnect()
					UnequippedConnection = nil
				end
				if HealthMonitorConnection then
					HealthMonitorConnection:Disconnect()
					HealthMonitorConnection = nil
				end
				
			end)
		end
	}
end

return SQHitbox
--// TOOL SCRIPT AND EXAMPLE AND MODULE DEMO
local Main = script.Parent
local Player = script.Parent.Parent.Parent --// The script starts while in the player's backpack
local SQHitbox = require(game.ServerStorage.SQHitbox).New(Player,Main) --// I named the module "SQHitbox"
local wait = task.wait

SQHitbox.SetDamage(25)

local deb = false
Main.Activated:Connect(function()
	if deb then return end
	deb = true
	for count = 1,1000 do
		if Player.Character.Humanoid.Health <= 0 or not Player.Character:FindFirstChild("HumanoidRootPart") then break end
		SQHitbox.CreateHitbox(Player.Character.PrimaryPart.CFrame * CFrame.new(math.random(-35,35),math.random(-5,5),math.random(-35,35)),Vector3.new(6,6,6),0,2)
		wait()
	end
	wait(1)
	deb = false
end)

Here’s a video of what the code above creates:

I put way too much effort into these posts lol.

1 Like

I just now realized what i did wrong, I forgot to put that one script inside of the bindable event :skull_and_crossbones:
My apologies, i should’ve read your other post multiple times. Sorry for wasting your time making that entire module script.

well, it works now, it uses an independent thread, Though i have some questions.

  • Is this fine for perfomance? lets say there are 15 players, all with a sword, and they spam it, So a hitbox per player is being created, therefore the bindable event is fired. is this alright?
  • Is there a way to tell the bindable event who the character using the hitbox is? If so, i could probably code it to delete the hitbox when they die/leave.

All that being said, thank you for all your help! (ill mark the one that helped me the most as the solution btw)

Because RaycastHitbox’s Raycast positions rely on the weapon’s animation to move.
Everytime you get damaged (in my game) depending on the weapon you got hit with, it’ll make you play a damaged animation (like being smacked or something) So the weapon’s hitbox wouldn’t follow up correctly.

hhhhhhhhhhh oh well, things happen. Eh don’t worry about it too much. I enjoyed making the script anyway lol.

Any performance loss caused just by using a BindableEvent is negligible/essentially nonexistent. I would only be concerned about dents in performance if there were hundreds of hitboxes operating at the same time, but I’m sure that will never happen.

Indeed there is. Take a look at the module I wrote and how I passed in the player and created a PlrCharacter variable I used later in CreateHitbox(). The function passed into the BindableEvent just needs to have or be able to access a variable to the player and/or character. Here’s a more bite-sized example of what I mean:

local Main = script.Parent
local Player = script.Parent.Parent.Parent --// Player variable

Main.Activated:Connect(function()
	game.ServerScriptService.SpawnIndependentThread:Fire(function()
		Main:Destroy() --// Destroy the tool
		wait(2)
		print(Player.Name) --// Using the Player variable from earlier; would print "ZoomSummit" for me
		local PlrCharacter = Player.Character --// Making it a variable just 'cause
		print(PlrCharacter.ClassName) --// Prints "Model"
	end)
end)
1 Like

Thank you!

Probably wont, Considering the max player count will probably be around 30 or so, all the tools have 1 hitbox operating at a time, So the max would be 30.

Anyways, Thank you so much for your help!

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.