Docs / Learn / Create a world

Create a world (and when to pick one)

Register a persistent arena, understand when a world beats a match, and prove one comes into being.

So far the arena has lived inside a match. A match is an arena round: a bounded, ephemeral fight. A fixed roster joins, your fighter moves, the round finishes, and the state is gone. That is the right shape for a round of play with a clear start and end.

A world is the other shape: a persistent arena. It is an always-on, zoned, shared space that never ends. It exists independently of any one player. People wander in and out; the arena stays. Under the hood the world is split into zone processes so it can carry many more players than a match, each player only receiving updates from the zones they can see.

Match or world?

Pick a match when:

  • Play happens in bounded rounds with a start and a finish.
  • The roster is small and fixed for the round.
  • Nothing needs to survive after the last player leaves.

Pick a world when:

  • The space is a shared room that outlives any single session.
  • Players join and leave continuously rather than as one cohort.
  • You expect more players than a single tick loop should simulate (a world fans out across zones; a match does not).

For the arena: a match is fine for a two-player round. Reach for a world when the arena becomes a persistent space players drift through and it should still be there when they come back. Matches are bounded rounds; a world is a persistent arena. The rest of this part builds the persistent-arena version.

See World server for the zone model and supervision tree, and Large worlds for tile-based worlds with lazy zone loading.

Write world.lua

A world script is a match script with world callbacks. The whole file is game logic, so it is identical on Cloud and self-hosted - you write it once.

-- lua/world.lua
game_type   = "world"
match_size  = 1
max_players = 500
grid_size   = 5
zone_size   = 400
tick_rate   = 50
view_radius = 1

function init(config)
    return { spawned = 0 }
end

function join(player_id, state)
    state.spawned = state.spawned + 1
    return state
end

function leave(player_id, state)
    return state
end

function spawn_position(player_id, state)
    return { x = 100 + math.random(200), y = 100 + math.random(200) }
end

function post_tick(tick, state)
    return state
end

That is the minimum a world needs: init, join, leave, spawn_position, and post_tick are all required. spawn_position returns the {x, y} where a joining player's fighter appears. Moving your fighter and broadcasting deltas comes in the next two steps; here the world just has to exist and accept a spawn.

Gotcha

the Lua global is game_type, not type. A script that sets type = "world" is silently registered as a match mode, and world.find_or_create then answers mode_not_found. (The Erlang sys.config key is type; only the Lua loader reads game_type.)

match_size is required by the loader for every mode, worlds included. Use 1 for a world that should not gate on a minimum player count.

Register the mode

Map a mode name to the script in config.lua. This manifest is game logic too, so it is the same on Cloud and self-hosted.

-- lua/config.lua
return {
    hub = "world.lua"
}

When config.lua is present, Asobi reads it instead of looking for a top-level match.lua. The mode is now named hub, and clients create worlds of it by that name.

Worlds are listed = true by default, so instances show up in world.list; quick_play defaults to true, so world.find_or_create may place a caller into an existing hub world. Both are covered in World server if you need to change them.

How a world is created

A world is not created at boot. It is created on demand by a client frame over the WebSocket, and the caller is auto-joined:

  • world.create {mode} - always make a new world.
  • world.find_or_create {mode} - return the first non-full world of that mode, or make one if none exists. This is the "drop me into a shared arena" call, and what you almost always want for the arena.

Both refuse with world_capacity_reached (global cap) or player_world_limit_reached (per-player cap). Those caps are the only Cloud/self-hosted difference in this step:

  • Cloud: the platform supplies defaults; you tune nothing to get started.
  • Self-hosted: set them in sys.config - {world_max, 1000} and {world_max_per_player, 5}. See World capacity in Configuration.

Everything else about creating a world - the frame you send and the world.joined reply you get back - is identical on both, and identical across every client SDK apart from the base server URL. The full SDK-by-SDK join flow is step 11; here we only need to see a world exist.

Checkpoint

A world exists once a client creates one. Deploy the bundle, then, reusing the authenticated WebSocket you connected in step 3 and the guest identity from step 4, send one frame:

{"type": "world.find_or_create", "payload": {"mode": "hub"}}

You should receive a world.joined push carrying the new world's id:

{"type": "world.joined", "payload": {"world_id": "...", "mode": "hub", "grid_size": 5, "max_players": 500, "player_count": 1, "status": "running"}}

Confirm it independently by listing worlds of the mode:

{"type": "world.list", "payload": {"mode": "hub"}}
{"type": "world.list", "payload": {"worlds": [{"world_id": "...", "mode": "hub", "player_count": 1, "max_players": 500}]}}

One hub world in the list means the mode loaded correctly and a live world exists. If you instead get mode_not_found, re-check game_type = "world" in world.lua.

Next: Step 11: Connect to a world

Join across every client SDK and receive the initial snapshot.