Skip to main content

Services

Services Defined

Services are singleton provider objects that serve a specific purpose on the server. For instance, a game might have a PointsService, which manages in-game points for the players.

A game might have many services. They will serve as the backbone of a game.

For the sake of example, we will slowly develop PointsService to show how a service is constructed.

Creating Services

In its simplest form, a service can be created like so:

local PointsService = Knit.CreateService { Name = "PointsService", Client = {} }

return PointsService
Client table optional

The Client table is optional for the constructor. However, it will be added by Knit if left out. For the sake of code clarity, it is recommended to keep it in the constructor as shown above.

No client table forces server-only mode

If the Client table is omitted, the service will be interpreted as server-side only. This means that the client will not be able to access this service using Knit.GetService on the client.

The Name field is required. This name is how code outside of your service will find it. This name must be unique from all other services. It is best practice to name your variable the same as the service name (e.g. local PointsService matches Name = "PointsService").

The last line (return PointsService) assumes this code is written in a ModuleScript, which is best practice for containing services.

Adding methods

Services are just simple tables at the end of the day. As such, it is very easy to add methods to services.

function PointsService:AddPoints(player, amount)
-- TODO: add points
end

function PointsService:GetPoints(player)
return 0
end

Adding properties

Again, services are just tables. So we can simply add in properties as we want. In our above method, we are returning 0 for GetPoints() because we have nowhere to store/retrieve points. Likewise, our AddPoints() method can't do anything. Let's change that. Let's create a property that holds a table of points per player:

PointsService.PointsPerPlayer = {}

Using methods and properties

Now we can change our AddPoints() and GetPoints() methods to use this field.

PointsService.PointsPerPlayer = {}

function PointsService:AddPoints(player, amount)
local points = self:GetPoints(player) -- Current amount of points
points += amount -- Add points
self.PointsPerPlayer[player] = points -- Store points
end

function PointsService:GetPoints(player)
local points = self.PointsPerPlayer[player]
return if points ~= nil then points else 0 -- Return 0 if no points found for player
end

Using events

What if we want to fire an event when the amount of points changes? This is easy. We can assign an event named PointsChanged as a property of our service, and have our AddPoints() method fire the event:

-- Load the Signal module and create PointsChanged signal:
local Signal = require(Knit.Util.Signal)
PointsService.PointsChanged = Signal.new()

-- Modify AddPoints:
function PointsService:AddPoints(player, amount)
local points = self:GetPoints(player)
points += amount
self.PointsPerPlayer[player] = points
-- Fire event signal, as long as we actually changed the points:
if amount ~= 0 then
self.PointsChanged:Fire(player, points)
end
end

Another service could then listen for the changes on that event:

function SomeOtherService:KnitStart()
local PointsService = Knit.GetService("PointsService")
PointsService.PointsChanged:Connect(function(player, points)
print("Points changed for " .. player.Name .. ":", points)
end)
end

KnitInit and KnitStart

In that last code snippet, there's an odd KnitStart() method. This is part of the Knit lifecycle (read more under execution model). These methods are optional, but very useful for orchestrating communication between other services.

When a service is first created, it is not guaranteed that other services are also created and ready to be used. The KnitInit and KnitStart methods come to save the day! After all services are created and the Knit.Start() method is fired, the KnitInit methods of all services will be fired.

From the KnitInit method, we can guarantee that all other services have been created. However, we still cannot guarantee that those services are ready to be consumed. Therefore, we can reference them within the Init step, but we should never use them (e.g. use the methods or events attached to those other services).

After all KnitInit methods have finished, all KnitStart methods are then fired. At this point, we can guarantee that all KnitInits are done, and thus can freely access other services.

In order to maintain this pattern, be sure to set up your service in the Init method (or earlier; just in the ModuleScript itself). By the time KnitStart methods are being fired, your services should be available for use.

Cleaning Up Unused Memory

Alright, back to our PointsService! We have a problem... We have created a memory leak! When we add points for a player, we add the player to the table. What happens when the player leaves? Nothing! And that's a problem. That player's data is forever held onto within that PointsPerPlayer table. To fix this, we need to clear out that data when the player leaves. We can use the KnitInit method to hook up to the Players.PlayerRemoving event and remove the data:

function PointsService:KnitInit()
game:GetService("Players").PlayerRemoving:Connect(function(player)
-- Clear out the data for the player when the player leaves:
self.PointsPerPlayer[player] = nil
end)
end

While memory management is not unique to Knit, it is still an important aspect to consider when making your game. Even a garbage-collected language like Lua can have memory leaks introduced by the developer.

Client Communication

Alright, so we can store and add points on the server for a player. But who cares? Players have no visibility to these points at the moment. We need to open a line of communication between our service and the clients (AKA players). This functionality is so fundamental to Knit, that it's where the name came from: The need to knit together communication.

This is where we are going to use that Client table defined at the beginning.

Methods

Let's say that we want to create a method that lets players fetch how many points they have, and when their points change. First, let's make a method to fetch points:

function PointsService.Client:GetPoints(player)
-- We can just call our other method from here:
return self.Server:GetPoints(player)
end

This creates a client-exposed method called GetPoints. Within it, we reach back to our top-level service using self.Server and then invoke our other GetPoints method that we wrote before. In this example, we've basically just created a proxy for another method; however, this will not always be the case. There will be many times where a client method will exist alone without an equivalent server-side-only method.

Under the hood, Knit will create a RemoteFunction and bind this method to it.

On the client, we could then invoke the service as such:

-- From a LocalScript
local Knit = require(game:GetService("ReplicatedStorage").Packages.Knit)

local PointsService = Knit.GetService("PointsService")
PointsService:GetPoints():andThen(function(points)
print("Points for myself:", points)
end)

Events (Server-to-Client)

We can use remote signals to fire events from the server to the clients. Continuing with the previous PointsService example, let's create a signal that fires when a client's points change. We can use Knit.CreateSignal() to indicate we want a signal created for the service.

local PointsService = Knit.CreateService {
Name = "PointsService",
Client = {
PointsChanged = Knit.CreateSignal(), -- Create the signal
},
}
Remote Signal

See the RemoteSignal documentation for more info on how to use the RemoteSignal object.

Under the hood, Knit is using the Comm module, which is creating a RemoteEvent object linked to this event. This is a two-way signal (like a transceiver), so we can both send and receive data on both the server and the client.

We can then modify our AddPoints method again to fire this signal too:

function PointsService:AddPoints(player, amount)
local points = self:GetPoints(player)
points += amount
self.PointsPerPlayer[player] = points
if amount ~= 0 then
self.PointsChanged:Fire(player, points)
-- Fire the client signal:
self.Client.PointsChanged:Fire(player, points)
end
end

And from the client, we can listen for an event on the signal:

-- From a LocalScript
local Knit = require(game:GetService("ReplicatedStorage").Packages.Knit)

local PointsService = Knit.GetService("PointsService")

PointsService.PointsChanged:Connect(function(points)
print("Points for myself now:", points)
end)

Events (Client-to-Server)

Signal events can also be fired from the client. This is useful when the client needs to give the server information, but doesn't care about any response from the server. For instance, maybe the client wants to tell the PointsService that it wants some points. This is an odd use-case, but let's just roll with it.

We will create another client-exposed signal called GiveMePoints which will randomly give the player points. Again, this is nonsense in the context of an actual game, but useful for example.

Let's create the signal on the PointsService:

local PointsService = Knit.CreateService {
Name = "PointsService",
Client = {
PointsChanged = Knit.CreateSignal(),
GiveMePoints = Knit.CreateSignal(), -- Create the new signal
},
}

Now, let's listen for the client to fire this signal. We can hook this up in our KnitInit method:

function PointsService:KnitInit()

local rng = Random.new()
-- Listen for the client to fire this signal, then give random points:
self.Client.GiveMePoints:Connect(function(player)
local points = rng:NextInteger(0, 10)
self:AddPoints(player, points)
print("Gave " .. player.Name .. " " .. points .. " points")
end)

-- ...other code for cleaning up player data here
end

From the client, we can fire the signal like so:

-- From a LocalScript
local Knit = require(game:GetService("ReplicatedStorage").Packages.Knit)

local PointsService = Knit.GetService("PointsService")

-- Fire the signal:
PointsService.GiveMePoints:Fire()
Client Remote Signal

See the ClientRemoteSignal documentation for more info on how to use the ClientRemoteSignal object.

Unreliable Events

Knit also supports UnreliableRemoteEvents, which is a special version of RemoteEvent. UnreliableRemoteEvents are, as the name suggests, unreliable. When an event is fired on an UnreliableRemoteEvent, the order and delivery of the event is not guaranteed. The listener of the event may receive the events out of order, or possibly not at all.

Having unreliable events is useful in scenarios where the data being sent is not crucial to game state. For example, setting the tilt rotation of each avatar's head: if some packets are dropped, this won't affect actual gameplay. The benefit is that unreliable events take up less network bandwidth.

To create an unreliable event, use Knit.CreateUnreliableSignal() within the client table of a service:

local MyService = Knit.CreateService {
Name = "MyService",
Client = {
PlayEffect = Knit.CreateUnreliableSignal(),
},
}

Using the unreliable signal is the same as normal ones (see the two sections above on events).

Properties

It is often useful to replicate data to all or individual players. Instead of creating methods and signals to communicate this data, RemoteProperties can be used.

For example, let's refactor the AddPoints method to set a RemoteProperty of the number of points the player has. The client will then be able to easily read this property:

-- Create the RemoteProperty:
PointsService.Client.Points = Knit.CreateProperty(0)

function PointsService:AddPoints(player, amount)
local points = self:GetPoints(player)
points += amount
self.PointsPerPlayer[player] = points
self.Client.Points:SetFor(player, points)
end

On the client, we can now easily read the Points property:

-- LocalScript
local Knit = require(game:GetService("ReplicatedStorage").Packages.Knit)

local PointsService = Knit.GetService("PointsService")

-- The 'Observe' method will fire for the current value and any time the value changes:
PointsService.Points:Observe(function(points)
print("Current number of points:", points)
end)

Using Observe is the easiest way to track the value of a RemoteProperty on the client.

Remote Property

See the RemoteProperty and ClientRemoteProperty documentation for more info on how to use the RemoteProperty and ClientRemoteProperty objects.


Full Example

PointsService

At the end of this tutorial, we should have a PointsService that looks something like this:

local Knit = require(game:GetService("ReplicatedStorage").Packages.Knit)
local Signal = require(Knit.Util.Signal)

local PointsService = Knit.CreateService {
Name = "PointsService",
-- Define some properties:
PointsPerPlayer = {},
PointsChanged = Signal.new(),
Client = {
-- Expose signals to the client:
PointsChanged = Knit.CreateSignal(),
GiveMePoints = Knit.CreateSignal(),
Points = Knit.CreateProperty(0),
},
}

-- Client exposed GetPoints method:
function PointsService.Client:GetPoints(player)
return self.Server:GetPoints(player)
end

-- Add Points:
function PointsService:AddPoints(player, amount)
local points = self:GetPoints(player)
points += amount
self.PointsPerPlayer[player] = points
if amount ~= 0 then
self.PointsChanged:Fire(player, points)
self.Client.PointsChanged:Fire(player, points)
end
self.Client.Points:SetFor(player, points)
end

-- Get Points:
function PointsService:GetPoints(player)
local points = self.PointsPerPlayer[player]
return points or 0
end

-- Initialize
function PointsService:KnitInit()

local rng = Random.new()

-- Give player random amount of points:
self.Client.GiveMePoints:Connect(function(player)
local points = rng:NextInteger(0, 10)
self:AddPoints(player, points)
print("Gave " .. player.Name .. " " .. points .. " points")
end)

-- Clean up data when player leaves:
game:GetService("Players").PlayerRemoving:Connect(function(player)
self.PointsPerPlayer[player] = nil
end)

end

return PointsService

Client Consumer

Example of client-side LocalScript consuming the PointsService:

-- From a LocalScript
local Knit = require(game:GetService("ReplicatedStorage").Packages.Knit)
Knit.Start():catch(warn):await()

local PointsService = Knit.GetService("PointsService")

local function PointsChanged(points)
print("My points:", points)
end

-- Get points and listen for changes:
PointsService:GetPoints():andThen(PointsChanged)
PointsService.PointsChanged:Connect(PointsChanged)

-- Ask server to give points randomly:
PointsService.GiveMePoints:Fire()