Build a Tag Arena¶
A four-player game of tag: everyone runs around the arena, one player is “it”, and touching
someone passes it on. This tutorial focuses on playing with more than two peers: reacting
to net.state changes with net.on, handling players who join mid-game, and
cleaning up after players who leave. Do Build Multiplayer Pong and Build a Coin Rush
first – the session menu comes from the former and the host-provisioning pattern from the
latter, and this tutorial only shows what is new.
The complete code is there to compare against when you are done.
What you will build¶
A host/join menu for up to four players
One colored square per player; whoever is “it” gets a red ring
Host-refereed tagging, announced through a
net.statechange listenerA taunt button using
net.emitCorrect behavior when players join mid-game or leave (even the tagged one)
Everything is drawn with fill_rect() and rect() – no sprites needed.
Step 1: Assemble the known parts¶
Start from the Pong session menu (max_players = 4, title "Tag arena") and Coin
Rush’s host-provisioning: the same provision_player with its first-writer idiom, minus
the score field, and the same my_player() / update_movement() pair for
arrow-key movement clamped to the screen. You will also want an AABB overlap test between two
players – Tag defines its own two-player overlaps(a, b) (shown in Tag Arena – complete code), the
same rectangle-overlap test explained in Current Limitations.
New locals for this game: tag_cooldown (host only) and taunt_timer, plus two
constants – IT_SPEED slightly above SPEED (the chaser’s reward) and
TAG_COOLDOWN = 60 frames. In update_movement(), pick the speed by role:
local speed = SPEED
if net.state.it == net.id() then
speed = IT_SPEED
end
Step 4: A taunt, because events still have a place¶
Not everything belongs in net.state. A taunt is a one-shot moment with no lasting truth
– exactly what net.emit is for. On space, with a one-second local cooldown so holding
the key does not spam:
if key_pressed(" ") and taunt_timer <= 0 then
taunt_timer = 60
net.emit("taunt")
print("You taunt everyone!")
end
Receivers subscribe to "event:taunt" in on_connected and print who taunted them
(the from argument). The local print next to the emit is the usual reminder that
senders never receive their own events.
Step 5: Play with three or four¶
Wire the remaining glue exactly as in the previous tutorials (winner-less this time: the game just runs), then put the multi-peer claims to the test:
Host on one machine, pick public visibility, and have two or three friends find the session in the join dialog’s public list.
Let one player join after the game has been running: they appear instantly with the right positions and the right “it” – the state snapshot doing its job.
Close the tab of the player who is “it”: the host’s
peer.leftremoves their square and takes the “it” role back.Close the host’s tab instead: everyone else gets the
"ended"message and recovers to the menu.
How it all fits together¶
Every player Host only
--------------------------- -----------------------------
writes own players.<id>.x/y provisions players (join/mid-game)
net.on("it", ...) announces removes them on peer.left
net.emit("taunt") -------> checks overlap with "it"
draws all players, writes net.state.it on a tag
ring around the "it" (replicates to everyone)
Complete code¶
Compare your build against the full documented script.
Extending the example¶
Freeze tag – tagged players stop moving until a teammate touches them; store a
frozenflag per player, written by the host.Score by survival – host counts the frames each player spends not being it in
net.state.players.<id>.score.Obstacles – draw a wall layout with
fill_rect()and block movement against it; keep the layout in constants so every client agrees.Round timer – host counts down in
net.state.time_left; whoever is “it” at zero loses.