Where Code Goes, and Where It Runs
The same eight lines of Luau will run for everyone, run for one player, or never run at all, decided entirely by which object you dropped them under. Move one script between four parents and see which of them start it.
Worth reading first: Everything Is an Instance in a Tree
The same eight lines of Luau will run for everyone, run for one player, or never run at all, decided entirely by which object you dropped them under. Move one script between four parents and see which of them start it.
A script is an instance like everything else
A script is not a file attached to your project. It is an object in the tree, exactly like a part, with a Name, a ClassName, and a Parent. That is not a technicality — it is the whole reason placement decides behaviour.
It also means a script can be moved, copied, cloned, and destroyed by other scripts while the game is running, and that a script sitting inside a part travels with that part when you duplicate it. Copy your laser and you have copied its killing logic too, which is exactly what you want.
Three script classes, and what each is for
Runs on the server. Everything that affects all players — damage, scoring, spawning — belongs here, because the server is the only copy everybody shares.
Runs on one player's own machine. Camera control, key presses, and interface. Nothing here can be trusted, because the player owns the computer it runs on.
Runs when something calls require() on it, and never on its own. Returns one value — usually a table of functions — so several scripts can share one implementation.
A ModuleScript is the one that surprises people. Putting one somewhere sensible and waiting does nothing at all; it is a library, and libraries do not start themselves.
-- ReplicatedStorage/ObbyUtil (a ModuleScript)local ObbyUtil = {} function ObbyUtil.getHumanoid(part) return part.Parent:FindFirstChildWhichIsA("Humanoid")end return ObbyUtil-- Any Script or LocalScript that wants itlocal ReplicatedStorage = game:GetService("ReplicatedStorage")local ObbyUtil = require(ReplicatedStorage.ObbyUtil) local humanoid = ObbyUtil.getHumanoid(somePart)The return at the bottom is mandatory. A ModuleScript with no return hands back nil, and every script that requires it fails on the next line with a message about indexing nil — an error that points at the caller and never at the module that caused it.
Where you put it decides whether it runs
Roblox does not run every script it finds. Each script class only starts inside particular containers, and outside them the script sits there looking completely normal and doing nothing.
There is no warning for this. The Explorer shows the script, the code is valid, the Output window stays empty, and nothing anywhere tells you that the object you chose as a parent is not one that starts anything.
game.Workspace
└─ Script "Hello.luau"
Runs — on the server
Runs on the server. This is where a killbrick's script lives — inside the part it belongs to, so the part and its behaviour move together.
Seven of these twelve placements do nothing. Nothing in Studio warns you about any of them — the script sits in the Explorer looking exactly like one that works, and the Output window stays empty.
RunContext loosened the old rule
For most of Roblox's history the rule was simply "class decides the side". A Script was server, a LocalScript was client, and where each would start was fixed.
Modern Script instances have a RunContext property with three values, which changes that. Legacy is the historical behaviour above and is still the default. Server and Client make the script run in that context from wherever it sits, including containers that would previously have ignored it.
print is your first and best instrument
Before anything else, put a print at the top of every script you write. Not for debugging a problem — for answering, instantly and permanently, the question that precedes every other question: did this run at all?
print("KillScript loaded on", workspace.Obby.Laser:GetFullName()) local laser = script.Parentlaser.Touched:Connect(function(otherPart) print("touched by", otherPart.Name)end)Two prints, and between them they answer nearly everything a beginner gets stuck on. Silence on the first means the script never started, and the problem is placement. The first without the second means the script ran but the event never fired, and the problem is the part. Those are entirely different bugs, and without the prints they look identical.
GetFullName() is worth the extra characters — it prints the whole path, so a message from one of six duplicated lasers tells you which one.
Key takeaways
- A script is an instance in the tree, not a file attached to the project. That is why its parent decides its behaviour.
- A script inside a part travels with that part when you duplicate it, which is how one laser becomes six.
- Script runs on the server, LocalScript on one player's machine, ModuleScript only when something requires it.
- A ModuleScript must end with return. Without one it hands back nil, and the error appears in the caller rather than the module.
- Seven of the twelve common class-and-parent combinations do nothing at all, and Studio warns you about none of them.
- RunContext on modern Scripts can override the placement rules. Leave it on Legacy while learning, so other people's tutorials still apply.
- Put a print at the top of every script. Silence means it never ran; that is a different bug from an event that never fired.
- print(obj:GetFullName()) tells you which of six identical copies is talking.
Quick check
Answer these to unlock the next chapter — 3 of 4 to pass. You can retake it anytime.
Answer every question to check.
Make a free account to read on
Every chapter is free — an account is how your progress, XP, and streak follow you from your laptop to your phone, and how you show up on the leaderboard. No payment, no trial.