Skip to content

Multiplayer

The rule that makes multiplayer simple: the host decides. One machine (the host, or a dedicated server) owns the truth: where things are, who scored. The others send requests and show what they're told. In KKE that rule is spelled with a file name.

Host scripts and player scripts

A script whose name starts with sv_ runs only where the truth lives: on the host, or when you're playing alone. Every other script runs on every machine. So a feature is usually two files: the host's sv_ script that decides, and a plain script that asks and shows.

This scoreboard is the smallest complete example. J asks for a point; the host counts it (no more than two a second per player, so a modified client can't cheat) and sends the totals to everyone.

net_scores/sv_scores.lua
-- sv_scores.lua: the scoreboard's truth. sv_ scripts run only where the
-- game's truth lives: on the host, or when playing alone. Players ask for
-- points; this decides, keeps the totals and tells everyone.
-- (docs/cookbook/networking.md)

local scores = {}   -- player id -> points

local function publish()
  -- To every player's machine (does nothing when playing alone)...
  if net and net.connected() then net.send("scores", scores) end
  -- ...and to the scripts on this one.
  hook.Run("ScoresChanged", scores)
end

-- The host decides: a point is only counted when it's allowed. Here, one
-- per player per half second, so a hacked client can't flood it.
local last = {}
local function addPoint(player)
  local now = kke.time()
  if last[player] and now - last[player] < 0.5 then return end
  last[player] = now
  scores[player] = (scores[player] or 0) + 1
  publish()
end

hook.Add("NetMessage", "sv_scores.point", function(name, data, from)
  if name == "point" then addPoint(from) end
end)
-- The host's own player (or you, playing alone) asks without the network.
hook.Add("WantPoint", "sv_scores.local", function(player) addPoint(player) end)
net_scores/scores.lua
-- scores.lua: every player's side. J asks for a point; the HUD shows the
-- scoreboard the host sends. Runs on every machine.
-- (docs/cookbook/networking.md)

input.define("scores.point", "Score a point", "J")

local hud = ui and ui.open([[
<rml><head><style>
  body { width: 100%; height: 100%; font-family: Noto Sans; color: #ffffff; pointer-events: none; }
  #board { position: absolute; right: 20dp; top: 20dp; padding: 8dp 16dp; background-color: #10131ecc;
           border-radius: 8dp; font-size: 18dp; }
</style></head>
<body><div id="board">Press J to score</div></body></rml>
]])

local function me() return net and net.playerId() or 0 end

local function show(scores)
  if not hud then return end
  local lines = {}
  for player, points in pairs(scores) do
    local who = player == me() and "You" or ("Player " .. player)
    table.insert(lines, string.format("%s: %d", who, points))
  end
  table.sort(lines)
  ui.text(hud, "board", table.concat(lines, "  ยท  "))
end

hook.Add("Think", "scores.keys", function()
  if not input.pressed("scores.point") then return end
  if net and net.role() == "client" then
    net.send("point", true)          -- ask the host
  else
    hook.Run("WantPoint", me())      -- we are the host (or alone): sv_scores.lua hears this
  end
end)

-- The host's copy, on the host; the network's copy, on a client.
hook.Add("ScoresChanged", "scores.show", show)
hook.Add("NetMessage", "scores.receive", function(name, data)
  if name == "scores" and type(data) == "table" then show(data) end
end)

Download sv_scores.lua Download scores.lua

  • net.send(name, data) goes from a player to the host, or from the host to every player. data is a number, text, true/false, or a table of those (up to 1 KB).
  • hook.Add("NetMessage", ...) receives, with the message's name, its data and who sent it (from, a player id).
  • net.role() is "host", "client" or "offline". Playing alone, the sv_ script is right there, so the scores go through hook.Run instead of the network; the same code works in both cases.
  • Never trust a message: the host checks it makes sense (here: not too often) before acting. What arrives damaged or oversized is dropped before a script sees it.

Both files go in the same scripts/ folder. Hosting, joining and leaving is in the game's menu (the starter game has it); loading and unloading the sv_ scripts as your role changes is automatic.

What replicates by itself

What an sv_ script spawns (physics.box, breakable.box, thrown balls) appears on every player's machine and moves as it does on the host, so a level built by a host script is shared without a line of network code. Plain scripts spawn locally: each machine makes its own.

The character's movement is server-authoritative (the host checks each player's moves), and voice chat, LAN discovery and join codes come with NetModule. Networking has the model in full; Server hosting runs the same scripts on a dedicated server, where server.* adds say, kick and a PlayerJoin hook; Anti-cheat is why the host decides.

Next: play to make.