There’s some confusion about how scripts behave when they’re deleted, so I figured I’d explain it in one giant post and forget about it.
This assumes that you understand coroutines. If you don’t, go read up on that now.
Scripts, Events, and Threads
So, when a Script or LocalScript is deleted, how does it just stop working?
When a script runs for the first time, the game engine creates an initial ‘thread’ from the script’s main body. Before this thread starts, the game engine assigns it a ‘script context’, which ties it to the script that it was created from. As it creates event hooks ( event:Connect(function(...)
), they are also assigned to the script context, so that when they are later fired, new threads are created and assigned to the script context. Any event hooks created in those threads are bound to the script context and so on.
Ultimately, all the threads and event hooks are bound to the same script context, so that when the script is later deleted, the game engine breaks all the event hooks and doesn’t resume yielding threads, stopping the entire script.
Let’s take this script for example:
-- game.ServerScriptService.ScriptA
wait(3)
local player = game.Players:GetPlayers()[1]
local humanoid = player.Character.Humanoid
humanoid:GetPropertyChangedSignal("Health"):Connect(function()
wait(0.2)
print(player.Name .. "'s Health: " .. humanoid.Health)
end)
wait(2)
humanoid.Health = humanoid.Health / 2
After 3 seconds, this starts listening to an event, and then after another 2 seconds, takes away half your health, but what happens specifically:
- The initial thread is started for the entire script and is assigned to a script context.
- The thread yields at
wait(3)
, allowing other stuff to run. - After the thread resumes, the event is connected, so the event connection is also assigned to the script context.
- The thread again yields at
wait(2)
. - The player’s
Health
is halved, immediatly triggering the event hook and spawning a brand new thread, which is assigned to the same script context as the event hook. - The new thread yields at
wait(0.2)
. - The inital thread immediately ends. The game engine isn’t tracking it anymore.
- The new thread resumes after 0.2 seconds and then prints the player’s health.
- The new thread ends and is no longer tracked.
- The event hook is fired repeatedly over the next several seconds as another script slowly heals the player, spawning a new thread each time.
So, what happens if another script deletes this script after 5.1 seconds from the start?
-- game.ServerScriptService.ScriptB
wait(5.1)
script.Parent.ScriptA:Destroy()
The event hook is already created and the initial thread is done; it’s not running anymore, and the event thread is 0.1 seconds into the wait(0.2)
command.
So when the script is deleted, the script context is destroyed, and so the associated event hook is broken and won’t fire anymore. The task scheduler managing wait(0.2)
sees the thread belongs to the dead context, and therefore never resumes it. The script never reaches a print
statement, so it doesn’t print anything.
ModuleScript Confusion
The behavior is great for those older games where scripted bricks get blown up and later deleted, or weapons get dropped into the void as they’re animating, but when ModuleScripts are used, it becomes a mess, because things break when a deleted script originally called a function in a ModuleScript to start a mechanism which serves multiple other scripts.
Let’s say for example there is a Script that starts a loop in a ModuleScript and then deletes itself:
-- game.ServerScriptService.Script
local module = require(script.Parent.ModuleScript)
module.Start()
wait(4)
script:Destroy()
-- game.ServerScriptService.ModuleScript
local module = {}
module.IsRunning = false
module.Counter = 0
function module.Start()
if not module.IsRunning then
module.IsRunning = true
spawn(function(...)
while true do
module.Counter = module.Counter + 1
print(module.Counter)
wait(1)
end
end)
end
end
return module
It would seem that once started, the loop should run forever because it is physically located in the ModuleScript’s Start
function.
Nope! That’s not what happens. Instead, the loop increments module.Counter
to 4 and then stops.
This is because the thread running module.Start()
belongs to the regular Script’s context, so the game engine associates the spawn
loop with that script instead of the ModuleScript. This means when that regular script is deleted, the ModuleScript’s loop stops.
To work around this, you can hook into a BindableEvent before returning the module and fire it from the triggering script so the infinite loop runs independent of the triggering script.
Alternatively, you can run module.Start()
from a script that never gets destroyed.
Indestructible ModuleScripts
Similar to regular Scripts, a ModuleScript’s context begins at its main script body, and ends once the module returns. Anything done during that time is associated with the ModuleScript.
However, unlike regular Scripts, a ModuleScript only runs when require
is called on it, and once running, cannot be stopped by conventional means. Specifically, the game engine doesn’t destroy the context when ModuleScript:Destroy()
is done, and there is no Disabled property to toggle. This means the ModuleScript’s behaviors can be made indestructible!
This behavior can be used to run the ModuleScript from nil, but unless you create self-destruct mechanisms, the only way to stop the ModuleScript is to stop the game itself.
Here is an example:
local module = {}
module.Counter = 0
spawn(function(...)
-- This runs forever, because it's started under the ModuleScript's context!
while true do
module.Counter = module.Counter + 1
print(module.Counter)
wait(1)
end
end)
return module