Video Version
Text Version
UI 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.Contents
- ResetOnSpawn
- StarterGui vs. PlayerGui
- WaitForChild
- Changing UI From the Server
- Remotes!
- RemoteFunctions
ResetOnSpawn
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 false
.
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:
game.Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("ScreenGui"):WaitForChild("TextLabel").Text="yeehaw v2"
WaitForChild
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 pcall
s and coroutine
s 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.
Remotes!
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 FireServer
.
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.
RemoteFunctions
Haven’t had enough yet? Let’s talk about RemoteFunction
s. A 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 RemoteFunction
into ReplicatedStorage
. On the client, we can just call the InvokeServer
function, saving the result to a variable, and then setting the result as the TextLabel
's text:
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.