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
endThat 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.