First Project
This walkthrough builds one working feature end to end: a coin counter that gives every player 100 coins five seconds after they join, replicates the total to their UI in real time and lets the client fetch it on demand. It touches a Service, a Controller, a Signal, a Property and a Client method, the four pieces you'll reuse in almost everything you build with OwlKnit.
Drop these two files into the structure from the previous page: CoinService.lua under Services/, CoinController.lua under Controllers/.
1. The Service
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Owl = require(ReplicatedStorage.OwlKnit.Owl)
local CoinService = Owl.CreateService({
Name = "CoinService",
Client = {
CoinsChanged = Owl.CreateSignal(),
TotalCoins = Owl.CreateProperty(0),
},
})
CoinService._data = {}
function CoinService:OwlInit()
print("[CoinService] Init.")
end
function CoinService:OwlStart()
print("[CoinService] Started.")
end
function CoinService:OwlOnPlayerAdded(plr: Player)
self._data[plr.UserId] = 0
self.Client.TotalCoins:SetFor(plr, 0)
-- > // Give 100 coins after 5 seconds
task.delay(5, function()
if game.Players:GetPlayerByUserId(plr.UserId) then
self:AddCoins(plr, 100)
end
end)
end
function CoinService:OwlOnPlayerRemoving(plr: Player)
self._data[plr.UserId] = nil
end
function CoinService:AddCoins(plr: Player, amount: number)
local current = self._data[plr.UserId]
if current == nil then return end
local new = current + amount
self._data[plr.UserId] = new
self.Client.TotalCoins:SetFor(plr, new)
self.Client.CoinsChanged:Fire(plr, amount, new)
end
function CoinService.Client:GetMyCoins(plr: Player): number
local token = Owl.GetPlrToken(plr)
if not token then return 0 end
return CoinService._data[plr.UserId] or 0
end
return CoinServiceA few things worth pointing out before moving to the client:
OwlOnPlayerAddedis one of Owl's automatic hooks, noPlayers.PlayerAdded:Connect(...)boilerplate, Owl calls it for you and it also fires for players already in the game when the Service starts.TotalCoinsis a Property: its value is replicated automatically, so the client never has to poll for it.CoinsChangedis a Signal: fired once per coin gain, for one-off reactions like a floating+100animation, that's the distinction between the two.CoinService.Client:GetMyCoinsis a Client method, a typed RemoteFunction the Controller can call andawaitlike a normal async function. Notice theOwl.GetPlrToken(plr)check: it confirmsplris the same player who actually invoked the remote, guarding against a spoofed argument.
2. The Controller
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Owl = require(ReplicatedStorage.OwlKnit.Owl)
local CoinController = Owl.CreateController({ Name = "CoinController" })
function CoinController:OwlInit()
self.CoinService = Owl.GetService("CoinService")
end
function CoinController:OwlStart()
-- > // Observe the Property (UI always up to date)
self.CoinService.TotalCoins:Observe(function(total)
print("Total:", total)
end)
-- > // Listen to the Signal (gain animation)
self.CoinService.CoinsChanged:Connect(function(added, total)
print(string.format("+%d coins! (Total: %d)", added, total))
end)
-- > // Fetch the current coin count on startup
self.CoinService:GetMyCoins()
:andThen(function(coins)
print("Coins at login:", coins)
end)
:catch(warn)
end
return CoinControllerObserve fires immediately with the current value and again on every future change exactly what you want for driving a UI label. Connect only fires on new events going forward, which is why it's the Signal used for the transient "coins just changed" animation rather than the persistent total.
Notice the Controller never touches
self.CoinService._datait can't, that table only exists on the server. Everything the Controller sees came throughClient.CoinsChanged,Client.TotalCoinsorClient:GetMyCoins(), which is the whole point of the Services & Controllers boundary.
3. Run it
If Init.server.lua and Init.client.lua are already pointed at the Services/Controllers folders (see Installation), there's nothing else to wire up press Play. You should see, in order: [CoinService] Init. and [CoinService] Started. in the server output, Total: 0 and Coins at login: 0 on the client and five seconds later, +100 coins! (Total: 100) followed by the Property's Observe callback firing again with the new total.
Where to go next
- Services and Controllers - the full API surface for what you just used.
- Signals and Properties - when to reach for one over the other.
- Client Methods - more on
Owl.GetPlrTokenand why it matters for any remote that takes aPlayerargument.