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 Scripts | ClientScripts | |
|---|---|---|
| A change to a part | Reaches every player | Stays on that player's screen |
players.me, input, camera, the player's gui | - | Yes |
data (saving), chat.send, achievements, destruction | Yes | - |
| ServerService and StorageService | Yes | Never: 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
-- ClientScript: ask to buy a sword
net.send("buy", "Sword")-- 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
-- Script
net.send(player, "coinCollected", player.stats.Coins) -- one player
net.sendAll("roundStarted", 2) -- everyone-- 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:
-- 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)-- ClientScript
local ok, message = net.call("redeem", box.text)
resultLabel.text = messageAn 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
-- 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)-- 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
| Container | Who has it | Put here |
|---|---|---|
| World | Server and every player | Parts, and Scripts that belong to a part |
ReplicationService (shared) | Server and every player | Models to clone, Modules both sides use |
| ServerService | Server only | Game logic Scripts, secret Modules |
| ClientService | Every player | ClientScripts not tied to UI |
| StorageService | Server only, nothing runs | Models and Modules for server Scripts |
| UI | Every player | Canvases 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
if game.isServer then print("server") end
if game.isClient then print("a player's device") endEvery frame
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
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.