Core concepts
The primitives Asobi gives you to build multiplayer games. Each concept shows the Lua API side by side with the Erlang equivalent — they're the same thing, just different surfaces on the same behaviour.
Games and modes
A game is a container: a name, a set of modules, a database schema. You register game modes within a game — each mode is one module implementing the asobi_match behaviour. A single game can have many modes (deathmatch, tutorial, ranked).
Matches
A match is one running session of a mode. 2–500 players, bounded lifetime, authoritative state. Each match is an Erlang process — if it crashes, other matches keep running.
Each match runs a tick loop (default 10 Hz). Your tick callback advances state; handle_input processes player actions; get_state serialises per-player views. The minimal game is a few dozen lines:
function game.tick(state)
state.elapsed = state.elapsed + 0.1
if state.elapsed >= 60 then
state._finished = true
state._result = { winner = leader(state) }
end
return state
endtick(#{elapsed := E} = State) when E >= 60 ->
{finished, #{winner => leader(State)}, State};
tick(#{elapsed := E} = State) ->
{ok, State#{elapsed := E + 0.1}}.Worlds and zones
For games with shared persistent space — MMOs, open-world survival, sandbox — use the world server. A world is divided into a grid of zones; each zone is its own process managing entities within its region. Players subscribe to nearby zones (interest management) and only receive updates from those.
Zones can be lazy-loaded (spawned on first access, reaped when empty) and paired with terrain chunks served on zone entry. Benchmarked at 500 real WebSocket players on a 128K×128K tile map at 208MB RAM.
-- spawn a goblin and find nearby players
local g = game.zone.spawn("goblin_warrior", 100, 200, { hp = 150 })
local nearby = game.spatial.query_radius(g.x, g.y, 50)%% same, Erlang
{ok, G} = asobi_world_server:spawn_at(World,
<<"goblin_warrior">>, {100, 200}, #{hp => 150}),
{ok, Nearby} = asobi_zone:query_radius(Zone, {100, 200}, 50).Matchmaking
Players enter a queue with skill/region/mode properties. A pluggable strategy module groups compatible players and spawns a match. Built-in strategies: fill (first-come-first-matched) and skill-based (MMR-bucketed, widens window over time).
Matchmaking is driven by the client over WebSocket (matchmaker.add → server replies with matchmaker.matched). Server-side, you can also enqueue from Erlang:
{ok, TicketId} = asobi_matchmaker:add(PlayerId, #{
mode => <<"ranked">>,
properties => #{skill => 1250, region => <<"eu_west">>}
}).Voting
Real-time voting during a match — for boon picks, path choices, map votes. Five methods: plurality, approval, weighted, ranked-choice, and spectator-weighted. Supports veto tokens, quorum early-resolution, and frustration bonuses for repeatedly losing voters.
From Lua, votes are opened by implementing the vote_requested(state) callback — return a vote config and the match server starts the vote. From Erlang, you can open one directly:
{ok, _} = asobi_match_server:start_vote(MatchPid, #{
template => <<"boon_pick">>,
method => plurality,
options => [<<"fireball">>, <<"shield">>, <<"speed">>],
window_ms => 30000
}).Phases, timers, seasons
Phases split a match into stages (lobby, active, results), each with duration and start conditions. Timers let you schedule one-shot or repeating events. Seasons wrap longer lifecycles (weekly competitive, monthly events).
Phases fire for Erlang match games and for Lua world games. Lua match games should model phases inside tick with an explicit state field.
-- Lua: world mode only
function game.phases(_config)
return {
{ name = "lobby", duration = 30000 },
{ name = "active", duration = 300000 },
{ name = "results", duration = 15000 },
}
endphases(_Config) ->
[
#{name => <<"lobby">>, duration => 30000},
#{name => <<"active">>, duration => 300000},
#{name => <<"results">>, duration => 15000}
].Chat, presence, DMs
Chat channels (world, zone, DM) are server-side and scoped per match/world. Presence tracks who's online via pg — cross-node out of the box in a cluster. Direct messages have their own lifecycle and persistence.
game.chat.send("world:main", player_id, "gg")asobi_chat_channel:send_message(<<"world:main">>, PlayerId, <<"gg">>).Economy and leaderboards
First-class wallet, store listings, IAP, inventory, and transactional ledger. Leaderboards support multiple scoring modes and time windows. Tournaments tie leaderboards to seasonal resets.
game.economy.grant(winner_id, "gold", 50, "match_win")
game.leaderboard.submit("arena:weekly", winner_id, kills)asobi_economy:grant(WinnerId, <<"gold">>, 50, #{reason => <<"match_win">>}),
asobi_leaderboard_server:submit(<<"arena:weekly">>, WinnerId, Kills).Reconnection
When a player disconnects, Asobi enters a grace period configured per game mode. During grace their entity can remain idle, become AI-controlled, or be marked invulnerable. If they reconnect in time, they resume seamlessly. If they don't, your game module decides (remove, forfeit, AI-takeover).
Hot reload
Deploy new code and it hot-swaps without disconnecting players. In-flight matches finish on the old code; new matches use the new code. Works the same way for Lua bundles and Erlang beam files — the BEAM's module system handles both.
Where next?
- Quick start — run the engine and ship a first game (Lua or Erlang).
- Lua API reference — the
game.*surface in full. - Erlang API reference — behaviours, modules, and specs.