Text VersionUI programming is one of those things that has some weird unintuitive quirks to it, but once you learn those quirks you can work with them fairly quickly. Today I’ll walk you through how to create UIs and control them with code, as well as how to properly change client UI from the server.
- StarterGui vs. PlayerGui
- Changing UI From the Server
ScreenGui objects have one very important property that we’ll want to change:
ResetOnSpawn. With the default
true value, the GUI will get completely reset whenever the player respawns. There are cases in which this is useful, but if we have a script running and suddenly all of the UI elements get replaced and all of our connections and references are now
nil, the script is going to throw an error real quick. For our use case, we’ll want this to be
StarterGui vs. PlayerGui
Now, if you try to change a UI element property in
StarterGui like you would some part in
workspace, you’ll quickly find out that it doesn’t work for some reason. Why? Well,
StarterGui isn’t actually what the player sees on their screen. Rather, it’s a template that players receive snapshots of when they join the game, and they can then the client code can control and change that snapshot. If you keep changing the template,
StarterGui, that won’t change the snapshots–the player would need to come back and take new ones. These “snapshots” are saved in
PlayerGui, a child of the
Player object. The proper way to change UI properties is like so:
In that code example, you may notice a large amount of
WaitForChild calls. Why not just reference the objects directly? Thing is, client scripts can execute before the player’s components are finished replicating, meaning you can’t always expect UI components to be in place at runtime. In cases like this, WaitForChild is your friend. However, once you’ve typed it out 50 times, it becomes tedious. You can reduce the amount of typing by saving references to these objects in variables, but that only helps so much. I have a
WaitForPath function available to make things simpler.
Changing UI From the Server
Here’s a seemingly correct example of showing a message to all clients from the server:
local message="asfugnaw9dfgndo9fgidogmo" for i,v in pairs(game.Players:GetPlayers()) do v:WaitForChild("PlayerGui"):WaitForChild("ScreenGui"):WaitForChild("TextLabel").Text=message end
However, directly changing player UI from the server is by far the worst way to go about this. There are dozens of cases in which this example could fail. Expecting all the clients to have decent connection speed and completely loaded UI is like having 5 unemployed roommates and expecting all of them to pay their share of the rent on time when your name’s on the lease, and also one of them takes two weeks to respond to your texts. It ain’t happening, basically.
There’s other issues with this too, like since the loop yields to wait for each client to be loaded, it’s very possible that a client can leave while the loop is executing, so when it gets to them, it breaks the script. Also, client-side UI changes don’t replicate back to the server, so if the server opens a menu for the player and there’s a client-side script controlling the close button, the server won’t be able to open the menu again because it will eternally perceive it as already open.
Yes, it’s technically possible to circumvent all of this stuff with
coroutines and sanity checks out the wazoo, but the amount of effort it takes to implement all that stuff is significantly less than the effort it would take to learn the proper way to do it.
Learning remotes may seem daunting at first glance, but it’s actually no more difficult than any other API. Since we’re just trying to send a message, and we don’t need to get anything back, we can use a
RemoteEvent. The function we’ll be using is
FireAllClients, which allows us to fire the event for every player at the same time–no loops required! If we wanted to send the message to only one player, we could use
FireClient, and if we wanted to send a message from the client to the server, we could use
We first need to add a remote. In my opinion,
ReplicatedStorage is the best place to store remotes, since it’s visible to both the client and the server. Technically, you could store them in the
Player object, but
ReplicatedStorage is much easier to access.
On the server, using the remote is as simple as calling the
FireAllClients function, passing through our message. Look at how clean this is.
local message="asfugnaw9dfgndo9fgidogmo" game.ReplicatedStorage.MessageBroadcast:FireAllClients(message)
On the client, we’ll connect to
OnClientEvent like we would any other event. We’ll receive the message and drop that into the text property of our label.
function messageReceived(message) game.Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("ScreenGui"):WaitForChild("TextLabel").Text=message end game.ReplicatedStorage.MessageBroadcast.OnClientEvent:Connect(messageReceived)
It’s that easy! We now have a significantly more stable system, and it only took a tiny bit more work.
Haven’t had enough yet? Let’s talk about
RemoteFunction is like a
RemoteEvent, except it’s actually designed to get a response from whatever receives the invocation. This does mean there are some limitations, including:
- The script invoking the function will yield until a result is returned
- Only one script can be listening to the remote at a time on either end
- You can’t invoke it for all clients at once
Actually, you just shouldn’t invoke it for clients at all. Similar to our bad way of changing UI from the server, invoking the client with a
RemoteFunction is dependent on them being able to receive the call and return the correct result, which is never 100% guaranteed. If you need to get client data, just have it send the data through a
RemoteEvent every once in a while, or if you need it at specific times, have the server ping the client with a
RemoteEvent and have the client set up to send the data back through the
RemoteEvent when pinged.
Again, we need to drop a
ReplicatedStorage. On the client, we can just call the
InvokeServer function, saving the result to a variable, and then setting the result as the
local message=game.ReplicatedStorage.GetMessage:InvokeServer() game.Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("ScreenGui"):WaitForChild("TextLabel").Text=message
The tricky part is on the server: instead of connecting to some kind of event, we’re going to set the
OnServerInvoke callback like it’s a property, providing a function to run when the server is invoked. The function technically receives a player argument, but we don’t need that in our case.
local message="asfugnaw9dfgndo9fgidogmo" game.ReplicatedStorage.GetMessage.OnServerInvoke=function() return message end
Congratulations, you know how to program UI on Roblox! And once you discover Roact, you’ll have to learn it all over again.