CommunicationBeginner

Coin & Rewards System

A Service tracking player currency with a Signal for gain animations and a Property replicating the live total.

SignalPropertyClient MethodsAutomatic Hooks
ServerScriptService/Services/CoinService.lua
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()
    -- > // Don't need to put something here, its gonna be overwrite by the framework
end
 
function CoinService:OwlStart()
    -- > // Same here
end
 
function CoinService:OwlOnPlayerAdded(plr: Player)
    self._data[plr.UserId] = 0
    self.Client.TotalCoins:SetFor(plr, 0)
 
    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 CoinService
StarterPlayerScripts/Controllers/CoinController.lua
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()
    self.CoinService.TotalCoins:Observe(function(total)
        updateCoinLabel(total)
    end)
 
    self.CoinService.CoinsChanged:Connect(function(added, total)
        playGainAnimation(added)
    end)
 
    self.CoinService:GetMyCoins()
        :andThen(function(coins)
            print("Coins at login:", coins)
        end)
        :catch(warn)
end
 
return CoinController
What's happening here
  • OwlOnPlayerAdded fires for players already present at startup, not just future joins no extra GetPlayers() loop needed.
  • TotalCoins (a Property) drives the UI via :Observe which replays the current value immediately on connection.
  • CoinsChanged (a Signal) fires once per gain the right tool for a one-shot animation, as opposed to persistent state.
  • GetMyCoins checks Owl.GetPlrToken before trusting the plr argument, guarding the RemoteFunction against a spoofed call.