Docs / Core concepts

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:

Lua

function game.tick(state)
    state.elapsed = state.elapsed + 0.1
    if state.elapsed >= 60 then
        return { finished = true, result = { winner = leader(state) } }
    end
    return state
end

Erlang

tick(#{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.

Lua

-- 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)

Erlang

%% 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).

Lua

-- Lua: enqueue and poll
local ticket = game.matchmaker.add(player_id, {
    mode = "ranked", skill = 1250, region = "eu_west"
})

Erlang

{ok, TicketId} = asobi_matchmaker:add(PlayerId, #{
    mode   => <<"ranked">>,
    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.

Lua

-- open a plurality vote for 30s
game.vote.start({
    template = "boon_pick",
    options = { "fireball", "shield", "speed" },
    duration_ms = 30000,
})

Erlang

{ok, _} = asobi_match_server:start_vote(MatchPid, #{
    template    => <<"boon_pick">>,
    method      => plurality,
    options     => [<<"fireball">>, <<"shield">>, <<"speed">>],
    duration_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).

Lua

-- declare phases for the mode
function game.phases(_config)
    return {
        { name = "lobby",   duration_ms = 30000 },
        { name = "active",  duration_ms = 300000 },
        { name = "results", duration_ms = 15000 },
    }
end

Erlang

phases(_Config) ->
    [
        #{name => <<"lobby">>,   duration_ms => 30000},
        #{name => <<"active">>,  duration_ms => 300000},
        #{name => <<"results">>, duration_ms => 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.

Lua

game.chat.send("world:main", player_id, "gg")

Erlang

asobi_world_chat:send(<<"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.

Lua

game.economy.grant(winner_id, "gold", 50, "match_win")
game.leaderboard.submit("arena:weekly", winner_id, kills)

Erlang

asobi_economy:grant(WinnerId, gold, 50, <<"match_win">>),
asobi_leaderboard: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?