Scripting Helpers is winding down operations and is now read-only. More info→
← Blog Home

It works in Studio, but not online!

This has got to be one of the most prevalent problems I've seen on Scripting Helpers. You make something in Studio, it works fine, but as soon as you try to play it online, it just doesn't work. In this post, I'll try to explain why most of these problems happen, and how to go about fixing them and preventing them in the future.


Two Worlds, One Game

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,

LocalScript
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 Touched.

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
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.

When 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

RemoteEvent
RemoteEvents and
RemoteFunction
RemoteFunction
s. More on that here.

Server-Restricted Services

If you look in the Explorer in studio mode, you'll see a bunch of services like

Workspace
Workspace,
Players
Players
,
Lighting
Lighting
, etc. However, some of these services are only accessible from server Scripts. The two that are important to us are
ServerScriptService
ServerScriptService
and
ServerStorage
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
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
ReplicatedStorage, which can be used for the opposite purpose: to store things that the client does need to download, such as
RemoteEvent
RemoteEvent
s and
RemoteFunction
RemoteFunction
s or prefabricated models.

ReplicatedFirst
ReplicatedFirst acts similarly to
ReplicatedStorage
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.

Server Scripts and LocalScripts

In ROBLOX, there are two ways a game developer can execute their code: either a

Script
Script, or a
LocalScript
LocalScript
.
ModuleScript
ModuleScript
s 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
Player object (such as PlayerScripts or PlayerGui), the player character in
Workspace
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
UserInputService just has one set of events, you don't specify a player you're looking to deal with. This is because the
UserInputService
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
DataStoreService or
HttpService
HttpService
, which can only be done via a server Script.

Which do I Use?

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
RemoteEvent inside the
ReplicatedStorage
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

Example

Conclusion

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:

Thanks for reading!

~ Evaera

Posted in Scripting Tips

Commentary

Leave a Comment

Perci1 says: December 29, 2015
I've always used a Changed event on a string value... this is probably better though.
lucas4114 says: January 2, 2016
@Perci1 Me too, I always do that.
DigitalVeer says: January 2, 2016
In summary, when you run a game inside of studio, everything is ran locally.
NinjaDogeGames says: January 9, 2016
i allways add the object inside the player througe the main script and have the local script wait until the object is parented then what i do is i set it as a var then parent it to what ever it needs to be parented to