When a player connects to an online ROBLOX game, sometimes it can feel like magic. However, in the background, there are actually two entirely separate versions of your game: the one that the player sees, and the one that the server sees. Every player has their own client world, and there is only one server world. For this example, we're going to assume that this server only has one player connected to it.
The world on the server will exist already when the server starts, but when a player joins, it will need to download the world from the server. Sometimes, LocalScripts will download to the client world and begin to run before other objects have been downloaded. This is a very common problem. To combat this, you should always use
WaitForChild when dealing with other objects in initial execution code. This isn't necessary if you know the code you're writing is running once the game has already been established, such as an event like
local gui = script.Parent:WaitForChild("MyTextLabelName")
However, the reverse can also occur. If you have a script that was listening to the
CharacterAdded event, for example, the event could have already fired before your function actually hooked into that event. It's usually good practice to always assume that the game has been running for a while, and you should run your event function manually on the objects (if they exist) your code deals with before hooking up the function connection.
function charAdded(model) -- Do stuff end if player.Character then charAdded(player.Character) end player.CharacterAdded:connect(charAdded)
By default, when the
FilteringEnabled property of Workspace is set to
false, any change that happens to one of these worlds is instantly sent to the other one. This is called "network replication". An important thing to note is that because the internet isn't instant, it will take time for changes and events to replicate over the network. I'll talk more about this in a bit.
FilteringEnabled is set to
true, however, things get a bit different. Changes from the client world will no longer replicate over the network to the server. (The server changes will still replicate to the client world.) You can still communicate with the server from the client world by using RemoteEvents and RemoteFunctions. More on that here.
If you look in the Explorer in studio mode, you'll see a bunch of services like Workspace, Players, Lighting, etc. However, some of these services are only accessible from server Scripts. The two that are important to us are ServerScriptService and ServerStorage. These two services are not present on the client, and their contents will not be replicated to the client world at all. These are the best places to put your game server Scripts, and content used in your game that's not currently active in the world, respectively. Since these services don't exist on the client world, it's impossible to acccess them directly via a LocalScript.
For example, if you had a level switcher, you could keep your currently inactive levels inside ServerStorage. This is great, because players won't have to download the inactive levels before they can get right into the action.
It would also be prudent to mention ReplicatedStorage, which can be used for the opposite purpose: to store things that the client does need to download, such as RemoteEvents and RemoteFunctions or prefabricated models.
ReplicatedFirst acts similarly to ReplicatedStorage, except it's the first thing to be downloaded when a player joins a game. You can use this to create things like custom loading GUIs. Here's a tutorial on doing just that.
In ROBLOX, there are two ways a game developer can execute their code: either a Script, or a LocalScript. ModuleScripts take on the behavior of whichever script type that they are included in.
Scripts run on the server world. LocalScripts run on the client world. LocalScripts will not even execute unless they're inside of a "local object", including (but not limited to) a member of a Player object (such as
PlayerGui), the player character in Workspace , or descendants of those mentioned.
Remember how we said that there is a lag time before things replicate over the two worlds? This is one of the reasons why you should never use a Script to do something that a LocalScript should do . It may work, but it will be way less snappy.
An example of this would be making a GUI appear when you click a button. If you put this code in a server Script, it has to tell the server that you pressed the button, and then the server has to tell your client to make the GUI appear. If you used a LocalScript, it could just set the GUI to be visible itself, and completely eliminate the lag time.
In addition, newer parts of the ROBLOX API can't even be used in a Script like this. For example, the UserInputService just has one set of events, you don't specify a player you're looking to deal with. This is because the UserInputService actually exists on the client world, and not the server. This means that there is only ever one player we're talking about, the one that owns the client world. (Protip:
game.Players.LocalPlayer is always a reference to the local player.)
An opposite example would be interacting with the DataStoreService or HttpService, which can only be done via a server Script.
When you're deciding whether or not to use a Script or a LocalScript to accomplish a certain task, it should never really come down to "opinion" or a "guess". Depending on what you're doing, one will always be more appropriate than the other.
You should use a Script when you're dealing with things that affect every player in the game, or the world of every player. This includes changing the server level, killing every player, admin commands, etc.
You should use a LocalScript when you're dealing with user input, or things that only one player will see. This includes changing the mouse icon, nearly every single script to power a GUI, player movement, camera manipulation, etc.
But: let's say you wanted to do something like... change something in a GUI of every single player? Which one do you use then? The answer: both!
Using a server Script to loop through the GUIs of every player is bad practice, because it introduces weird behavior in a lot of ways, such as unexpected delays in yield functions, players seeing different things at different times, and (worst of all), you've got GUI code way over in your server scripts, far away from the rest of your GUI code. Keeping your code clean and tidy is very important, otherwise you'll end up with a pile of spaghetti.
A good solution would be to use a RemoteEvent inside the ReplicatedStorage service. You have your LocalScript in the GUI have something like this:
-- We named our RemoteEvent "UpdateTextGui" local UpdateTextGui = game.ReplicatedStorage.UpdateTextGui -- Hook into the event and change the GUI text UpdateTextGui.OnClientEvent:connect(function(text) gui.Text = text end) -- Ask the server to send us the data immediately UpdateTextGui:FireServer()
Then, from a server Script:
local UpdateTextGui = game.ReplicatedStorage.UpdateTextGui -- Allow the clients to request the data immediately UpdateTextGui.OnServerEvent:connect(function(player) UpdateTextGui:FireClient(player, "Initial text!") end) -- Updates the text on screen of all players every 5 seconds while wait(5) do UpdateTextGui:FireAllClients("Hi there! " .. tick()) end
If you follow all of these tips, you should be able to make your game multiplayer-compatible from the start. I briefly covered a lot of these points, and there is always further reading (and experimenting) to be done with network filtering. Below you can find some resources with more information: