Multiplayer

Several players can play one DarkRoast game together, on the same network or over the internet. One player hosts and plays too, and the others join. Everyone sees the others as an animated body (the built-in cowboy, or your game’s own hero), walking, running and raising their gun as they do.

The multiplayer demo seen by the host: another player walking towards the camera between crates and red balls, with 'Hosting, 2 playing' in the corner

Try it

Download the Multiplayer Demo from the download page and unzip it. Start it and choose Host a game, then start it again and choose Join a game on this PC. On another PC on your network, run it from a command prompt with the host’s address:

"Multiplayer Demo.exe" --host
"Multiplayer Demo.exe" --join 192.168.1.20

Push the crates and balls into the green goal; the score is shared. The blue button (E) drops another ball for everyone. Any packaged game, and blendforge.exe itself, takes the same --host [port] and --join address[:port] options. The port is 27960 unless you give one.

How it works

Replicated objects

In Blender, tick Replicate (BlendForge tab → Object Components) on anything that moves and must be in the same place for everyone: crates, balls, doors, platforms, enemies. Objects that don’t move need nothing.

Scripting

net.host(port?, max_players?), net.join(address, port?), net.leave()Host, join or leave from a script, for example from a title menu’s buttons. A host takes up to 8 players unless told otherwise (net.max_players()); more are turned away. Joining happens in the background, and on_connected follows.
net.set_name("Will"), net.name()What this player is called; the others see it in net.players(). Sent when joining, and again when it changes. Keep it with save("name", ...).
net.advertise{game = "My Game", mode = "dm", started = false}(Host) What a LAN search shows about this game: any table. net.advertise(false) stops answering searches.
net.lan_games(game?)Games hosted on the local network, each {address, port, host, players, max, info} (host is the host player’s name, info what they advertise); only that game’s, if given. Call it every frame a game list is open: it keeps the search going. Join one with net.join(g.address, g.port).
net.active(), net.connected()Hosting or joining; actually in a game.
net.is_host(), net.is_client()Which side this machine is. In single player, both are false.
net.id()This player’s number: the host is 1, and the others are 2, 3… in order of joining.
net.players()Everyone in the game, each {id, position, entity, is_me, name, crouching}. entity is the body drawn for that player.
net.send("name", value, to?)A message to everyone else, or to one player’s id. The value can be a number, text, true/false, a vec3 or a table.
net.set("key", value), net.get("key", default?)State the host shares with everyone, such as scores and the round number. The host sets it; everyone reads it.
spawn_model("outlaw.glb", pos, yaw, true)On the host: loads a model file for everyone (with true). The others get a copy that follows the host’s, its own moving parts included (a walking enemy). Its scripts and AI run on the host only.
obj.net_id, net.object(id)The number every machine knows a replicated object by, and the object with that number here. Put it in a message to point at an object, e.g. which enemy a player hit.
net.give(obj, player_id), net.owner(obj), net.mine(obj)(Host) Hands a replicated object to a player: their machine simulates it (the car they drive, physics and all) and everyone else follows it. net.give(obj, 1) takes it back; a player who leaves gives theirs back. net.mine(obj): this machine moves it. The Racing Demo gives each player a car this way.
net.avatar("models/hero.glb")How the other players look: a model with player_idle, player_walk, player_run (and player_jump) actions. What’s in its right hand is its gun: when a player aims (player_aim), the others see the arm come up and point where they look. Show their gun of the moment through net.players()’s entity.
ui.input("Host's address", "127.0.0.1")A text box in a menu (inside ui.menu): returns what’s typed, for a Join button.

Animations the host’s scripts play on a replicated object, or on anything under one (obj:play_animation, crossfade, stop_animation, and auto_animate for Character Animation), play on every machine. Character Animation also runs on each machine by itself, from the movement it sees.

These callbacks work in the level script and in objects’ scripts. Objects’ scripts get self first.

function on_connected(id) end                 -- you joined (id: your number)
function on_disconnected(why) end             -- "refused" (no host there) or "lost"
function on_player_joined(id) end             -- someone joined (everyone hears it)
function on_player_left(id) end
function on_net_message(name, value, from) end

Who does what

Every machine runs the level’s scripts, so menus, HUD text, sounds and effects work as usual. Anything that should happen once, such as counting a goal, spawning a ball or opening a door, belongs on the host: check net.is_client(), and have clients ask the host with a message. The demo’s button:

function on_interact(self, player)
  if net.is_client() then
    net.send("spawn_ball", true, 1)           -- ask the host (player 1)
  else
    spawn("Ball", self.position + vec3(0, 0, 2.5))
  end
end

-- in the level script
function on_net_message(name, value, from)
  if name == "spawn_ball" and net.is_host() then
    spawn("Ball", find("BallButton").position + vec3(0, 0, 2.5))
  end
end

And its goal, counted on the host and shared with net.set:

function on_trigger_enter(self, other)
  if net.is_client() or other.is_player then return end
  net.set("goals", net.get("goals", 0) + 1)
  other:destroy()
end
-- everyone shows it: ui.set("goals", "Goals: " .. net.get("goals", 0))

Game lists and lobbies

Games hosted on the same network find each other: a host answers LAN searches (a UDP broadcast on port 27950, sent on every network adapter) with what it net.advertises, and net.lan_games lists the hosts that answered. A game list is a button per game:

for i, g in ipairs(net.lan_games("My Game")) do
  if ui.button("Join " .. g.host .. " (" .. g.players .. "/" .. g.max .. ")##" .. i) then
    net.join(g.address, g.port)
  end
end

A lobby is a few lines on top: the host shows net.players() and a Start button; Start sends a message and sets net.set("started", true); a player who joins and finds started false waits in the lobby, and one who joins later goes straight in. Data Rustlers’ start menu and lobby work this way.

A co-op shooter: Data Rustlers

Data Rustlers in co-op, seen by player 2: the host in his buckskin coat with his revolver raised, beside the cyan hologram marshal, on the neon street

Data Rustlers, on the download page, plays its whole slice in co-op for 2–4 players: Host co-op opens a lobby, and the others pick it from the list of games on their start menu (or join by address). It’s built on the same pieces:

Deathmatch

Data Rustlers also has a deathmatch for 2–4 players: Host deathmatch on the start menu (or switch the lobby to it), and the others join the same way. Same street, no outlaws, no bullet time; first to 10 kills, or the most after 6 minutes. It’s game script only, on the same pieces:

Over the internet

On a home network, games show up in the list by themselves. To join by address, use the host PC’s local address (ipconfig shows it, e.g. 192.168.1.20). Over the internet, the host forwards UDP port 27960 on their router to their PC, and players join the host’s public address. Windows Firewall asks the host to allow the game the first time; allow it on private networks.

Limits

The demo is built by samples\make_multiplayer_demo.py in the Samples & docs download, and samples\multiplayer_demo.blend has its scripts.