Service-like programming: Timer example

Introduction

Over the past year I have started to develop a new way of programming which has helped me a lot with writing back-end code. I have not seen many people who use a similar style of programming, so I thought it would be nice to share my way of programming with everyone. My style shares some similarities with OOP (Object Orientated Programming) but it also works really similar to service objects (e.g. Teams, Players, RunService) in the fact that:
  • 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

To fully understand what I will be talking about it is important that you have a decent understanding of the following things:
  • 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:

Roblox Timer with methods.png

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:

Roblox Timer with events.png

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:

Roblox Timer with properties.png

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!

31 Likes

I’m literally doing this exact thing in my project. It’s really neat to see someone come up with the same architecture as me!

2 Likes

A post was merged into an existing topic: Memes, Puns & GIFs mega thread

My Team Crazy Game Framework is a service-based system! It makes things like this suuuper practical and easy to implement. It hides all the need to mess with remote objects (it does it for you in the background).

1 Like

Some features may deter users who want to use your API. People dislike touching an API’s code because they shouldn’t have to navigate through it to fix its errors. If they do touch it, they risk jeopardizing their ability to install future updates, waste time, and are unsure of the original developer’s intentions.

One thing I would fix is the variables for your Listeners/ListenerLists. They’re undeclared.

Check out my newer AeroGameFramework on GitHub. I’ve revamped the whole system to work a lot better.

1 Like

That’s one of the issues I was referencing.

You didn’t allow ROBLOX to fetch the modules/model. You should make it available:

It’s another issue. You can copy + paste the framework file by file but developers would dislike that. When people decide on frameworks they look at two very key factors:

  • Documentation
  • Maintenance

I was pointing out the documentation isn’t on par and could lead to poor reviews.

However, cool framework.

I’m confused by what you’re talking about. It takes one line of code to install/update the entire framework, and I documented all the pieces of the framework on the GitHub page. I think documentation could be extended, and I have half of a video series done that will help people use it.

If you can point out specific issues with the code, I’d be happy to fix it. I’m not sure what you meant by fixing variables for the ListenerList being undeclared.

I dont get the complaint either your installation is easy and your documentation is quite extensive.

It might take a while to learn how to use the framework effectively. But that should be expected, you shouldnt install a framework anyway if you dont want to put in the effort of getting to know how it works.

Goodjob on the framework Crazyman!

Before the module wasn’t free to get, so installing required manually copy and pasting.
That was the gist.

1 Like

Yep, that was my bad. I accidentally made it not free when reconfiguring it a bit ago. Glad you caught that though; fixed that earlier

1 Like