Live Example

Asobi Arena

A multiplayer top-down arena shooter. Move, shoot, and survive. Built entirely on the asobi_match behaviour.

How it works

One Erlang module. ~270 lines. Full multiplayer arena.

🎮

800×600 Arena

Bounded play area with random spawn points. Players can't leave the arena.

💥

Projectile Combat

Aim and shoot with cooldown. Projectiles travel at 8 units/tick with collision detection.

100 HP / 25 Damage

Four hits to eliminate. Kills and deaths tracked per player.

90-Second Matches

Timed rounds with automatic scoring. Last player standing or highest kills wins.

Architecture

Each match runs as an isolated OTP process. The game module implements callbacks for the full lifecycle.

1

init/1

Set up arena state: empty player map, projectile list, match timer.

2

join/2 & leave/2

Spawn players at random positions with 100 HP. Clean up on disconnect.

3

handle_input/3

Process movement (WASD) and shooting (aim + fire) from each client.

4

tick/1

Server tick: move projectiles, detect collisions, remove out-of-bounds, check win condition.

The full match module

This is the actual game logic. Asobi handles networking, matchmaking, and state sync.

-module(asobi_arena_game).
-behaviour(asobi_match).

-export([init/1, join/2, leave/2, handle_input/3, tick/1, get_state/2]).

-define(ARENA_W, 800).
-define(ARENA_H, 600).
-define(MAX_HP, 100).
-define(DAMAGE, 25).
-define(GAME_DURATION, 90000).

init(_Config) ->
    {ok, #{players => #{}, projectiles => [],
           next_proj_id => 1, started_at => undefined}}.

join(PlayerId, #{players := Players} = State) ->
    Player = #{x => rand:uniform(700) + 50,
               y => rand:uniform(500) + 50,
               hp => ?MAX_HP, kills => 0, deaths => 0},
    {ok, State#{players => Players#{PlayerId => Player}}}.

handle_input(PlayerId, Input, State) ->
    %% Apply movement + shooting per client tick
    Player1 = apply_movement(Input, get_player(PlayerId, State)),
    {State1, Player2} = maybe_shoot(PlayerId, Input, Player1, State),
    {ok, put_player(PlayerId, Player2, State1)}.

tick(#{players := Players, projectiles := Projs} = State) ->
    Projs1 = move_projectiles(Projs),
    {Projs2, Players1} = check_collisions(Projs1, Players),
    State1 = State#{players => Players1,
                    projectiles => remove_oob(Projs2)},
    case time_up(State1) orelse one_standing(Players1) of
        true  -> {finished, build_result(Players1), State1};
        false -> {ok, State1}
    end.

Build your own game

Asobi Arena is just one example. Implement the asobi_match behaviour and build anything — racing, puzzle, RTS, battle royale.

Try it yourself

Get the arena running locally in a few minutes. You need Erlang/OTP 27+, PostgreSQL, and one of the game engines above.

1

Start the backend

git clone https://github.com/widgrensit/asobi_arena
cd asobi_arena
docker compose up -d
rebar3 shell
2

Clone a client demo

# Pick your engine:
git clone https://github.com/widgrensit/asobi-godot-demo
# or: asobi-unity-demo, asobi-defold-demo, asobi-flame-demo
3

Open the project and play

Open the demo in your engine, hit play, and register a player. Open a second window to matchmake against yourself. WASD to move, mouse to aim, click to shoot.

The backend runs on port 8084 by default. Check each demo's README for engine-specific setup.