Roblox EP3 Server and client: where Script, LocalScript and ModuleScript run
A Roblox game has two sides: the shared server and each player's device. See who sees which change, put code in the right place, and learn why it stops cheats.
- The server is the real game everyone shares. What the server changes, everyone sees
- What a player's device changes on its own, only that device sees. Fine for screens and effects, not for scores or money
- Anything that decides winning or losing must be decided by the server, because players' devices can be tampered with
In EP2 we put a Script inside each floor. It works, but we never asked where it runs. This episode answers that, because it is what makes multiplayer games behave correctly, and it is the heart of stopping cheaters. Skip it and you get strange bugs like “I can see it, but my friend can’t”.
One game, two sides
When people join your game, Roblox starts one server on Roblox’s machines. Everyone in that room connects to it, and each person also runs a copy of the game on their own device (called the client).
| Server | Player’s device (client) | |
|---|---|---|
| How many | one per room | one per player |
| Job | the real game; decides everything | display, button presses, camera, sound |
| Who sees its changes | everyone | that device only |
| Can players tamper with it | no | yes (this is where cheat tools work) |
Principle: the server is the truth of the game; each device is a window looking into it.
See it with your own eyes
Studio can simulate a multiplayer game on one machine:
- Open your game from EP2
- Go to the Test tab, find Clients and Servers, set players to 2, and press Start
- Studio opens three windows: one server and two players (Player1, Player2)
Now add player-side code. Stop the test first, then in Explorer open StarterPlayer → StarterPlayerScripts, press + and choose LocalScript:
local baseplate = workspace:WaitForChild("Baseplate")
baseplate.Color = Color3.fromRGB(255, 120, 200)
print("client: painted the floor pink")
Start the 2-player test again. Both player windows show a pink floor, but the server window still shows the old colour. The LocalScript runs on each player’s device, and each device paints its own floor; the server knows nothing about it.
Make the difference clearer by painting only for Player1:
local Players = game:GetService("Players")
local me = Players.LocalPlayer
if me.Name == "Player1" then
workspace:WaitForChild("Baseplate").Color = Color3.fromRGB(255, 120, 200)
end
Player1 sees pink, Player2 sees the old colour. That is what “only that device sees it” means.
Two new words in this code
workspace:WaitForChild("Baseplate"): player-side code starts very early, sometimes before the scene has finished loading. Usingworkspace.Baseplatedirectly can error with “not found”.WaitForChildwaits until the thing arrives. Simple rule: player-side code that refers to things in the scene uses WaitForChildPlayers.LocalPlayer: the player who owns this device. It only works in player-side code; on the server it is nil, because the server belongs to no player
Three kinds of code, in the right place
| Kind | Runs on | Put it in |
|---|---|---|
| Script | the server | ServerScriptService (recommended) or Workspace |
| LocalScript | the player’s device | StarterPlayerScripts, StarterCharacterScripts, StarterGui |
| ModuleScript | nothing by itself; something must call it | ReplicatedStorage if both sides use it, ServerStorage if only the server does |
A LocalScript in the wrong place, such as Workspace, simply never runs, with no error. Another bug beginners cannot find.
Newer Scripts have a RunContext property in Properties that picks Server or Client wherever the script sits. It is useful in big projects, but at the start, the table above causes less confusion.
ModuleScript: write once, use on both sides
Values many scripts share, such as the lava colour or how long a floor disappears, should not be retyped in every Script. Put them in one ModuleScript.
In ReplicatedStorage, press +, choose ModuleScript, name it Config:
local Config = {}
Config.LAVA_COLOR = Color3.fromRGB(255, 85, 0)
Config.FADE_SECONDS = 1
Config.GONE_SECONDS = 3
return Config
A ModuleScript must return one thing, usually a table holding several values. Any script that wants it uses require:
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Config = require(ReplicatedStorage:WaitForChild("Config"))
print(Config.GONE_SECONDS) -- 3
Server Scripts and player LocalScripts can both require the same one, because ReplicatedStorage is sent to every device. Want to change the game’s difficulty? Edit one place.
game:GetService("...") is the standard way to get the game’s core services, such as Players and ReplicatedStorage. Use it instead of game.Players; it always returns the right thing.
Why this stops cheating
Say a LocalScript adds coins when a player picks something up. A player with a cheat tool can edit the code on their own device and give themselves a million coins instantly.
The good news: if the game is designed properly, that cheat stays on their device. The server does not believe it, others do not see it, and it is not saved, because the real score is stored and changed on the server.
The rule for the whole series: never trust the player’s device. Scores, money, items, stages cleared, winning and losing: the server always decides.
One exception worth knowing: your own character is moved by your own device, which then tells the server where it is. Roblox does this so walking never lags. As a result, some cheat tools make characters run faster or fly, so serious games have the server check whether movement makes sense. That is beyond beginner level, but know it exists.
What about the code from EP2?
The lava and fading floors are Scripts, so they run on the server. That is correct: every player sees the floor vanish at the same moment, and nobody can stop lava from killing them.
What is not good yet is that they are spread across every part. With 50 lava slabs there are 50 identical Scripts, and every change means 50 edits. EP4 moves them all into one Script in ServerScriptService.
Try it in 10 minutes
- Write a LocalScript that paints the floor for Player1 only, run a 2-player test, and see what each window shows
- Make the
ConfigModuleScript, and changeFadePadfrom EP2 to readConfig.GONE_SECONDSinstead of the number 3 - Move the LocalScript into Workspace and test; you will see it does nothing at all
Common mistakes
- Handling scores or money in a LocalScript, so nobody else sees them and cheating is easy
- Putting a LocalScript in Workspace, where it silently never runs
- Player-side code without WaitForChild, erroring only sometimes (fine on fast machines, broken on slow ones)
- Using
Players.LocalPlayerin a server Script and getting nil
Next episode
Ready for a real game. EP4 puts together a complete obby: checkpoints that respawn you at your latest stage, a stage counter on the leaderboard, every lava slab in the game handled by one Script, and a finish line that counts wins.
FAQ
Why not put everything on the server?
Some things must react instantly on the player's device, such as on-screen buttons, the camera, sounds and effects. Asking the server first every time would add enough lag to spoil the fun.
Is a Script in Workspace different from one in ServerScriptService?
Both run on the server. The difference is that Workspace is also sent to players' devices, while ServerScriptService is not; players cannot see the code inside it, which makes it the right home for the game's main code.
Commands and settings were tested on sample systems. Try them on a test machine before production.