Introduction
- Services are unique, you cannot have more than one instance of it.
- And just like with services, my style uses incoming calls (methods) and outgoing calls (events).
Because of these similarities I will refer to my style as âservice-like programmingâ. In this tutorial I will explain what this style looks like, how it works and what you can do with it. I will be programming a timer to demonstrate all of this. This tutorial might be a little slow or not that useful for experienced programmers, but I hope that it will still bring something new to their tables.
Required Knowledge
- Basic understanding of Lua (functions, if-statements, loops etc.)
- What are BindableEvents and BindableFunctions and how do you use them?
- How events work in Roblox and how they are linked to functions.
Service-like programming
Roblox services can be interacted with in 3 ways: You can call methods, you can listen to events and you can read/write properties. With Service-like programming I will try to replicate these types of interactions, starting with the incoming calls (methods).
Methods
There are two types of methods:
- Getters
- Setters
âGettersâ are calls that expect one or more values to be returned. For example, you could have a method that asks the timer how much time is left. In that case you will of course want a number to be returned. Your call does not modify the timer in any way though, it only âgetsâ data. BindableFunctions are often used for this functionality.
âSettersâ are the opposite of getters. Instead of asking for data they are used to change data. A timer could have a setter method which directly âsetsâ how much time is left. Often times you will exactly know what data you are changing so you do not need any data to be returned. In that case you should use a BindableEvent. Sometimes however you will want to set data but still return some other other data, in that case you should use a BindableFunction.
In the case of the timer example for this tutorial, I will be using 1 getter and 2 setters. The setters will be âStartTimerâ and âStopTimerâ. The StartTimer method will be used to both set the time and start the timer. StopTimer will be used to both stop the timer and reset the time to zero. The getter will be âGetTimeLeftâ which asks the timer how much time is left. The timer itself will be programmed in a regular Script and it is useful to group the methods together into a Folder inside the Script so it becomes clear which object the methods belong to. The timer will now look something like this:
Now that we have the basic structure for our timer we can start programming it. Here is how I would personally implements these methods:
-----------------[ = VARIABLES = ]-----------------
local TimeLeft = 0
local TimerRunning = false
-----------------[ = API = ]-----------------
script.API.GetTimeLeft.OnInvoke = function()
return TimeLeft
end
script.API.StartTimer.Event:Connect(
function(StartAt)
TimeLeft = StartAt
TimerRunning = true
while TimeLeft > 0 do
-- wait one 'tick' and return how much time passed
local DeltaTime = game:GetService("RunService").Heartbeat:Wait()
TimeLeft = math.max(TimeLeft-DeltaTime, 0) -- we don't want TimeLeft to be negative
end
if TimerRunning == true then
TimerRunning = false
end
end
)
script.API.StopTimer.Event:Connect(
function()
TimeLeft = 0
TimerRunning = false
end
)
Given that methods are incoming calls, only the object to which those methods belong (the Timer script in this case) should âlistenâ to those BindableEvents and BindableFunctions. This is done by linking the âEventâ event of the BindableEvent to a function in the script or in the case of a BindableFunction, defining the âOnInvokeâ property of the BindableFunction as a function inside the script. All other scripts that want to use those methods should only call them at most. This is to preserve the actual functionality/definition of an incoming call.
There are a few awesome upsides of using BindableEvents and BindableFunctions for the communication with the Timer script in this way:
- You can immediately see which things you can or cannot use the timer for simply by reading the names of the BindableEvents and BindableFunctions in the Timerâs API folder.
- Any script within the same environment can use the Timer script. You can even have two seperate scripts indirectly communicate with each other this way (e.g. one script starts the timer and the other one reads how much time is left).
- Once the Timer works you can basically ignore any of the code inside the Script. All you have to know is what the BindableFunctions and BindableEvents in the API folder do.
If you paid some attention to the code I posted you might think âwhat is the purpose of the âTimerRunningâ variable when you already have TimeLeft?â The reason why the variable is there is because it makes implementing events easier, which is what we will be doing next.
Events
Events are basically the opposite of methods in the fact that they are outgoing calls instead of incoming. Right now we have to be really proactive with our communication towards the timer. If we want to know anything about the timer we have to index one of its methods and call those. This is annoying though when we want to know things like âwhen does the timer reach zero?â Of course we could call the âGetTimeLeftâ method, but if we were to implement another method to pause the timer things will get a little complex. Events are a simple solution for this problem.
To define events we will be using BindableEvents. Given that we already have BindableEvents for incoming calls it is important to seperate the events for easy identification. I will be doing this by inserting an âEventsâ folder in the âAPIâ folder and putting all the BindableEvents in there instead. Unlike with our methods we never want any script other than the script the events belong to to call those BindableEvents. This is also to preserve the actual functionality/definition of out outgoing call.
For the timer example I will simply be creating two events: âTimerStartedâ which is triggered when the timer is, well⌠started of course. The other event is âTimerStoppedâ which is called when the timer reaches 0, be it by calling âStopTimerâ or waiting for it to actually reach 0. The structure of our timer should now look something like this:
To implement those events we will of course have to adjust our previous code. Fortunately it will be easy with our current structure:
-----------------[ = VARIABLES = ]-----------------
local TimeLeft = 0
local TimerRunning = false
-----------------[ = API = ]-----------------
script.API.GetTimeLeft.OnInvoke = function()
return TimeLeft
end
script.API.StartTimer.Event:Connect(
function(StartAt)
TimeLeft = StartAt
TimerRunning = true
script.API.Events.TimerStarted:Fire(StartAt)
while TimeLeft > 0 do
-- wait one 'tick' and return how much time passed
local DeltaTime = game:GetService("RunService").Heartbeat:Wait()
TimeLeft = math.max(TimeLeft-DeltaTime, 0) -- we don't want TimeLeft to be negative
end
if TimerRunning == true then
TimerRunning = false
script.API.Events.TimerStopped:Fire("Automatically") -- timer stopped because it reached 0
end
end
)
script.API.StopTimer.Event:Connect(
function()
TimeLeft = 0
TimerRunning = false
script.API.Events.TimerStopped:Fire("Manually") -- timer stopped because 'StopTimer' was called
end
)
This new piece of code only has 3 new lines added to it. Can you spot them? I will help you:
- âscript.API.Events.TimerStarted:Fire(StartAt)â was added at the start of the second function (script.API.StartTimer.Event:ConnectâŚ). This line will call the TimerStarted BindableEvent and will also send the set time as a parameter. Now if we want to trigger some code when the timer is started we just have to listen to TimerStarted.Event!
- âscript.API.Events.TimerStopped:Fire(âAutomaticallyâ)â was added at the end of the second function. This line of code is triggered only when the timer reaches 0 by itself. It will also send a string âAutomaticallyâ as a parameter so we can identify that the timer was stopped by itself.
- âscript.API.Events.TimerStopped:Fire(âManuallyâ)â was added all the way at the bottom. When this line of code is triggered it will send âManuallyâ as a parameter to identify that the timer was stopped because the StopTimer method/BindableEvent was called.
Now that we support both methods and events for our timer we can do a lot of things. Are you working on a game and you want to have a 20 second intermission between rounds? With this timer it suddenly becomes really easy to make that! In fact, only two lines of code are needed!
Pseudo-code:
PathToYourTimer.API.StartTimer:Fire(Intermission_Duration)
PathToYourTimer.API.Events.TimerStopped.Event:Wait()
This code will start the timer at âIntermission_Durationâ - which is a number you will have to define by yourself - and the code will yield until the âTimerStoppedâ BindableEvent is fired. It is as simple as that.
Now that we have implemented both methods and events, we can round it off with one simple last step: properties.
Properties
Properties work fairly similar to getter and setter methods. Basically every Roblox service has properties. Most of these properties can both be set and read. Setting a property is like calling a setter method. Reading a property is like calling a getter method. The upside of properties is that they are easy to read and/or change. The downside of properties is that they only hold one value, so they are not âpowerfulâ.
Adding properties to our timer is fairly simple. I personally like to do this by adding a âConfigurationâ object to the timer script and filling them will value objects which will act as the properties. Even though value objects in Roblox are really outdated, they still hold value - no pun intended - in our example. Unfortunately Roblox does not really want us to parent Configuration objects to anything besides Model objects as of writing this, but you can simply drag and drop them into other types of objects. Alternatively you can use the command bar to instantiate them directly where you want them to be, which in this example would be the âTimerâ script. I will also be renaming the Configuration folder to âPropertiesâ and adding a BoolValue to it called âIsRunningâ which will tell us if our timer is currently running. The final structure of our timer object should now look like this:
The IsRunning property/boolean will only support reading functionality in this example. You will not be able to (un)toggle it to start or stop the timer, although that should not be too difficult to add by yourself. You can do it in around 10 lines of code in fact! Hint: if you want to try this yourself as practice, you should consider using the Changed event of the BoolValue and the StartTimer + StopTimer BindableEvents. To program the read functionality of the âIsRunningâ BoolValue all we have to do is add a few lines of code to set the value of this BoolValue. Given that we already have a TimerRunning variable we can use this to our advantage:
-----------------[ = VARIABLES = ]-----------------
local TimeLeft = 0
local TimerRunning = false
-----------------[ = API = ]-----------------
script.API.GetTimeLeft.OnInvoke = function()
return TimeLeft
end
script.API.StartTimer.Event:Connect(
function(StartAt)
TimeLeft = StartAt
TimerRunning = true
script.Properties.IsRunning.Value = true -- toggle the BoolValue
script.API.Events.TimerStarted:Fire(StartAt)
while TimeLeft > 0 do
-- wait one 'tick' and return how much time passed
local DeltaTime = game:GetService("RunService").Heartbeat:Wait()
TimeLeft = math.max(TimeLeft-DeltaTime, 0) -- we don't want TimeLeft to be negative
end
if TimerRunning == true then
TimerRunning = false
script.Properties.IsRunning.Value = false -- toggle the BoolValue
script.API.Events.TimerStopped:Fire("Automatically") -- timer stopped because it reached 0
end
end
)
script.API.StopTimer.Event:Connect(
function()
TimeLeft = 0
TimerRunning = false
script.Properties.IsRunning.Value = false -- toggle the BoolValue
script.API.Events.TimerStopped:Fire("Manually") -- timer stopped because 'StopTimer' was called
end
)
This final piece of code has three new lines added to it. After every âTimerRunning = trueâ or âTimerRunning = falseâ I added a line of code to set the value of âscript.Properties.IsRunning.Valueâ. You can now also use this BoolValue to find out if the timer is still running instead of calling the âGetTimeLeftâ BindableFunction.
This is pretty much the end of the tutorial. Thanks for reading and enjoy your free timer! I also hope that - even though it was a long read - there were some useful bits of knowledge in the tutorial.
If anyone wants me to make more tutorials let me know! If I made any mistakes in this tutorial let me know as well!