Skip to content

Server and client ​

Every KHIM game runs in two places at once:

  • The server - one per game session, run by KHIM. Its Scripts own the game: the world, health, coins, scores.
  • Each player's device - the KHIM app or the browser. Its ClientScripts handle that player's input, UI, camera and effects.

Who sees what ​

Server ScriptsClientScripts
A change to a partReaches every playerStays on that player's screen
players.me, input, camera, the player's gui-Yes
data (saving), chat.send, achievements, destructionYes-
ServerService and StorageServiceYesNever: they are not sent

So a door moved by a ClientScript opens for one player only - and since the server owns collisions, that player still bumps into it. Things that matter happen in Scripts.

The one rule

The server decides. A ClientScript can ask for something, but a player can run a modified app and send anything. A Script must never take an amount, a price, a target or a result from a player. It checks the player may do it now - near enough, alive, the cooldown passed, can afford it - and works the amount out itself.

Messages: net ​

net sends messages by name between the server and players. Values can be nil, booleans, numbers, strings, value types (vec3, colours...), parts, players and tables of those - up to 16 KB.

Player to server ​

lua
-- ClientScript: ask to buy a sword
net.send("buy", "Sword")
lua
-- Script: decide
local PRICES = {Sword = 50, Shield = 30}

net.on("buy", function(player, item)       -- the sender comes first
    local price = PRICES[item]
    if not price then return end            -- not something we sell
    if player.stats.Coins < price then return end
    player.stats.Coins -= price
    shared.Items[item]:clone().parent = player.backpack
end)

Server to players ​

lua
-- Script
net.send(player, "coinCollected", player.stats.Coins)   -- one player
net.sendAll("roundStarted", 2)                          -- everyone
lua
-- ClientScript
net.on("coinCollected", function(total)
    label.text = "Coins: " .. total
end)

Asking and waiting for the answer ​

net.call sends a question from a ClientScript and waits for the server's answer; net.handle answers it:

lua
-- Script
net.handle("redeem", function(player, code)
    if code == "WELCOME" and not player.stats.Redeemed then
        player.stats.Coins += 100
        player.stats.Redeemed = true
        return true, "100 coins added!"
    end
    return false, "That code doesn't work."
end)
lua
-- ClientScript
local ok, message = net.call("redeem", box.text)
resultLabel.text = message

An error in the handler becomes an error in the caller.

Limits ​

A player may send at most 120 messages a second; the server drops the rest. A number that is NaN or infinite refuses the whole message, and a part from ServerService or StorageService arrives as nil. Still - check everything a message says.

A worked example: a sword ​

lua
-- Script (ServerService): the client asks, the server decides
local lastSwing = {}

net.on("swing", function(player)
    local now = time()
    if (lastSwing[player.name] or 0) + 0.5 > now or player.health <= 0 then return end
    lastSwing[player.name] = now

    for _, other in players.all() do
        if other.name ~= player.name and (other.position - player.position).magnitude < 6 then
            other:damage(20)               -- the server's number, not the client's
        end
    end
end)
lua
-- ClientScript: send the click, nothing else
input.onKey("F", function()
    net.send("swing")
end)

The client sends no damage and no target: the server knows where everyone is.

Where to put scripts ​

ContainerWho has itPut here
WorldServer and every playerParts, and Scripts that belong to a part
ReplicationService (shared)Server and every playerModels to clone, Modules both sides use
ServerServiceServer onlyGame logic Scripts, secret Modules
ClientServiceEvery playerClientScripts not tied to UI
StorageServiceServer only, nothing runsModels and Modules for server Scripts
UIEvery playerCanvases and the ClientScripts that drive them

ClientScripts are not secret

A player receives every ClientScript and every Module they can require (with comments stripped out). Keep secrets - codes, admin lists, prices you check - in ServerService or StorageService.

Checking which side you're on ​

lua
if game.isServer then print("server") end
if game.isClient then print("a player's device") end

Every frame ​

lua
game.frame:connect(function(dt)   -- dt: seconds since the last frame
    spinner.rotation += vec3(0, 90 * dt, 0)
end)

On the server it fires every step (60 a second); on a player's device every frame drawn.

When the server shuts down ​

lua
game.closing:connect(function()
    -- every player is still here: save what the game keeps for everyone
end)

The server waits for closing handlers (and the players.left handlers that run after it) up to 20 seconds. See Saving data.