●intro.sh
●connection.gd
●peer_vs_steam.md
●rpcs.gd
●spawn_sync.tscn
●wrap_up.md
bearlikelion.com :: arneman.me :: markmakes.games :: GodotCon Boston 2026
fish /home/mark/Source/godotcon26


                      .:::          :::.
                  :-==++++:        :++++==-:
                  =+======+:      :+======+=
                  ==========================
                  ==========================
       :-:     :-============================-:     :-:
     .=+++=-::=++============================++=::-=+++=.
    -+=====+++==================================+++=====+-
   -+====================================================+-
    -====================================================-
     .==================================================.
      -+======+#%@%%#*==================*#%%@%#+======+-
      -+=====#@@#+=+*@%+=====++++=====+%@*+=+#@@#=====+-
      -+====*@@=:::::-@#=====%@@%=====#@-:::::=@@*====+-
      -+====*@@-::::::@#=====%@@%=====#@::::::-@@*====+-
      -+=====*@%+=-=+%#======%@@%======#%+=-=+%@*=====+-
      -+======+*#%##*+=======#@@#=======+*##%#*+======+-
      :=======================++=======================:
      +%####***++============================++***####%+
      =####%%%@@#========#%%%%%%%%%%#========#@@%%%####=
      :=======#@%=======+@@********@@+=======%@#=======:
      .+======*@@%%%%###%@%========%@%###%%%%@@*======+.
       :++=====+****######+========+######****+=====++:
        .-=++====================================++=-.
          .:-=++++==========================++++=-:.
              .:-===++++++++++++++++++++++===-:.
                    ..:::------------:::..


mark@laptop ~/Source/godotcon26 $ godot --path .
MAKING GAMES
MULTIPLAYER
A guide to understanding Godot's multiplayer
# Mark Arneman :: @bearlikelion :: GodotCon Boston 2026
mark@laptop ~/Source/godotcon26 $

##Hi, I'm Mark

whoami
$ whoami --verbose
  ___  _   _   _   _ ___   __  __    _    ____  _  ___
 / _ \| | | | | | | |_ _| |  \/  |  / \  |  _ \| |/ / |
| | | | |_| | | |_| || |  | |\/| | / _ \ | |_) | ' /| |
| |_| |  _  | |  _  || |  | |  | |/ ___ \|  _ <| . \|_|
 \___/|_| |_| |_| |_|___| |_|  |_/_/   \_\_| \_\_|\_(_)
Full Stack & Independent Game Developer
Two games on Steam Steam: SurfsUp (2025) :: SurvivalScape (2024)
├─ SurfsUp was developed with multiplayer from the start.
└─ SurvivalScape got multiplayer added in 2026.
24 games on itch.io itch.io :: 11 in the last year :: 9 featuring multiplayer
Contributed multiple fixes to GodotSteam & SteamMultiplayerPeer
$

##Come play at my booth

▸ Find me in the back right of the showcase area
▸ A custom launcher with 15 projects to play
▸ Play them and leave feedback right in the launcher
▸ Scan the QR to take it home :: markmakes.games/connect
▸ The launcher is entirely open source (MIT) :: github.com/bearlikelion/mmg-launcher
mpv launcher.mp4

##What is Cooties?

▸ An open source (MIT) multiplayer game of tag
▸ One player gets Cooties and spreads it to the others
▸ 5 rounds, highest score wins
▸ Built entirely with Godot 4's high-level multiplayer
▸ Developed in a single 16-hour sprint, no AI / agentic coding
mpv cooties_clips.mp4

##It's already spreading

mpv covino.mp4 --volume 100

▸ JustCovino on Twitch implemented Cooties' netcode for his game Astro Yoinkers :: wishlist it on Steam!

##It's reached production

discord :: incoming dm
You are going to love this. Got paid for a new project to help on. I get into the project...
godot -e :: the paid project
Godot editor showing cooties_reference.md inside the project's docs folder

##What we'll cover

cat agenda.md
$ cat agenda.md
1. Establishing a connection :: the strength of Godot's MultiplayerPeer
2. Who is who :: PEER ID vs STEAM ID, and why you need both
3. Talking to each other :: RPCs, politely yelling functions across the internet
4. Spawning & Setting Authority :: MultiplayerSpawner and who owns what
5. Staying in sync :: MultiplayerSynchronizer, then making it smooth

##Two autoloads run the show

Global.gd

  • players : the Dictionary of everyone connected
  • change_level(), players_synced signal
  • The source of truth, living on every peer

SteamInit.gd

  • Boots Steam with steamInitEx(480)
  • Pumps Steam.run_callbacks() every frame
  • Owns the SteamMultiplayerPeer
Pro tip: use folder colors. They're awesome.
Set Folder Color in the FileSystem dock

##One scene to rule them all

main.gd×
main.gd > _on_change_level
1class_name Main
2extends Node
3 
4@onready var level: Node = $Level
5 
6func _ready() -> void:
7 Global.change_level_to.connect(_on_change_level)
8 
9 
10func _on_change_level(new_level_path: String) -> void:
11 for child_level: Node in level.get_children():
12 child_level.queue_free()
13 
14 var new_level_resource: Resource = load(new_level_path)
15 if new_level_resource:
16 var new_level: Node = new_level_resource.instantiate()
17 level.add_child(new_level)
Scenes/main.gd17:1   GDScript
Main.tscn :: scene tree
▾Mainmain.gd
▾Level
 MainMenuinstanced scene
 LevelSpawner
 Music
swap, don't switch Global.change_level() emits a signal; Main swaps what lives under $Level.
why not change_scene_to_file()? It tears down the whole tree at a different moment on every peer. Node paths break mid-swap: unreliable for multiplayer.
same tree everywhere Main never dies: /root/Main/Level exists on every peer, every frame. Swaps arrive as call_local RPCs.
mark@laptop ~/Source/godotcon26 $ cd connection/
PART ONE
Establishing
a connection
Understanding the strength of Godot's MultiplayerPeer class

##One socket, many plugs

multiplayer.multiplayer_peer = <any MultiplayerPeer>

ENetMultiplayerPeer

  • IP + port, ships with Godot
  • Works on every platform
  • You handle the networking realities

SteamMultiplayerPeer

  • GodotSteam extension
  • Lobbies, relays, friends
  • Players never see an IP

Other plugs

  • NakamaMultiplayerBridge
  • EOSGMultiplayerPeer
  • OfflineMultiplayerPeer
Change the connection, keep the code.

##ENet vs Steam

ENetSteam
NAT traversalRequires port forwardingAutomatic
MatchmakingSend your IP to a friendLobbies built in
Relay serversNoneFree, "Valve pays"
PlatformsEverywhere Godot runsSteam only
Best forPrototypes, LAN, local testingReleased games & Spacewar (480)

##Letting the player pick

Cooties main menu backend dropdown

In Cooties the backend is just an OptionButton. One enum, one dropdown, and the host button matches on it.

main_menu.gd×
main_menu.gd > MultiplayerBackend
4enum MultiplayerBackend { ENET, STEAM }
⋮# ...
12@onready var backend: OptionButton = %Backend
⋮# ...
144func _on_host_game_pressed() -> void:
145 match backend.selected:
Scenes/MainMenu/main_menu.gd145:1   GDScript
Same buttons, same lobby, different plug.

##Hosting: one button, two backends

main_menu.gd×
main_menu.gd > _on_host_game_pressed
144func _on_host_game_pressed() -> void:
145 match backend.selected:
146 MultiplayerBackend.ENET:
147 print("Creating ENET Server")
148 var peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new()
149 var error: Error = peer.create_server(7777, 4)
150 if error:
151 print("Server Error: %s" % error)
152 else:
153 multiplayer.multiplayer_peer = peer
154 Global.add_local_player()
155 Global.change_level("res://Scenes/Lobby/lobby.tscn")
156 MultiplayerBackend.STEAM:
157 print("Hosting Steam Lobby")
158 Steam.createLobby(Steam.LobbyType.LOBBY_TYPE_PUBLIC, 4)
Scenes/MainMenu/main_menu.gd158:1   GDScript
ENet create_server(7777, 4) : port 7777, four players.
Steam No server yet! Ask Steam for a lobby, wait for the lobby_created signal to fire.

##Joining through ENet

main_menu.gd×
main_menu.gd > _on_connect_pressed
161func _on_connect_pressed() -> void:
162 var peer: ENetMultiplayerPeer = ENetMultiplayerPeer.new()
163 var error: Error = peer.create_client(ip_address.text, 7777)
164 if error:
165 print("Client Error: %s" % error)
166 connection_status.text = "Client Error: %s" % error_string(error)
167 return
168 
169 Global.ip_address = ip_address.text
170 multiplayer.multiplayer_peer = peer
171 
172 # Wait for player data to sync from server before entering the lobby
173 connection_status.text = "Connecting..."
174 multiplayer.connection_failed.connect(_on_connection_failed)
175 Global.players_synced.connect(_on_players_synced)
Scenes/MainMenu/main_menu.gd175:1   GDScript
same shape create_client(ip, 7777) : the mirror image of create_server.
patience Don't enter the lobby until the server has synced the player list via RPC. We'll talk about RPCs next.

##Leveraging Steam lobbies

main_menu.gd×
main_menu.gd > _on_lobby_created
62func _on_lobby_created(connected: int, lobby_id: int) -> void:
63 if connected == 1:
64 print("Created lobby %s" % lobby_id)
65 SteamInit.lobby_id = lobby_id
66 SteamInit.peer.host_with_lobby(lobby_id) # Use Steam MultiplayerPeer
67 multiplayer.multiplayer_peer = SteamInit.peer
68 
69 Steam.setLobbyJoinable(lobby_id, true)
70 Steam.setLobbyData(lobby_id, "name", Steam.getPersonaName() + "'s lobby")
71 Steam.setLobbyData(lobby_id, "game", "GodotCootiesMPTutorial")
72 
73 var set_relay: bool = Steam.allowP2PPacketRelay(true)
74 print("Allowing Steam to relay backup: %s" % set_relay)
75 
76 Global.add_local_player()
Scenes/MainMenu/main_menu.gd76:1   GDScript
the handoff host_with_lobby() then assign the same multiplayer_peer property. The game never knows the difference.
lobby metadata Name + game tag make your lobby findable by requestLobbyList() filters.
free insurance If P2P fails, Valve relays the traffic. This costs you nothing.

##Joining a Steam lobby

main_menu.gd×
main_menu.gd > _on_lobby_joined
88func _on_lobby_joined(lobby_id: int, _permissions: int, _locked: bool, response: int) -> void:
89 if response != Steam.CHAT_ROOM_ENTER_RESPONSE_SUCCESS:
⋮# ...
95 if Steam.getLobbyOwner(lobby_id) == Steam.getSteamID():
96 Global.change_level("res://Scenes/Lobby/lobby.tscn")
97 return
98 
99 SteamInit.lobby_id = lobby_id
100 SteamInit.peer.connect_to_lobby(lobby_id)
101 multiplayer.multiplayer_peer = SteamInit.peer
102 
103 # Wait for player data to sync from server before entering the lobby
104 connection_status.text = "Connecting..."
105 multiplayer.connection_failed.connect(_on_connection_failed, CONNECT_ONE_SHOT)
106 Global.players_synced.connect(_on_players_synced, CONNECT_ONE_SHOT)
Scenes/MainMenu/main_menu.gd106:1   GDScript
same shape again connect_to_lobby(), assign the peer, wait for the sync. ENet and Steam joins are twins.
wait a second getLobbyOwner() equals getSteamID()? That's a Steam ID, not a peer id. There are TWO id systems here...
mark@laptop ~/Source/godotcon26 $ diff peer_id steam_id
PART TWO
Who is who?
PEER ID vs STEAM ID: two id systems, one game

##Two kinds of ID

peer_id
$ multiplayer.get_unique_id()
1 # the host. ALWAYS 1
$ multiplayer.get_unique_id()
245023941 # a client. random per session
▸ Assigned at connect, forgotten at disconnect
▸ Targets RPCs, names nodes, owns authority
peer id = seat number this round
steam_id
$ Steam.getSteamID()
76561197984176210 # my real steam id
▸ 64-bit, globally unique
▸ Permanent: same account, same id, forever
▸ Identity: persona name, avatar, friends
▸ Lobbies and lobby ownership
steam id = passport

##Peer ID in action

Global.gd×
Global.gd > _on_connected_to_server
60# Called when this client successfully connects to server
61func _on_connected_to_server() -> void:
62 print("GLOBAL CONNECTED TO SERVER")
63 var local_id: int = multiplayer.get_unique_id()
64 
65 # Send our player name to the server
66 # The server will sync back to us after receiving our name
67 var player_name: String = str(local_id)
68 if multiplayer.multiplayer_peer is SteamMultiplayerPeer:
69 player_name = SteamInit.steam_name
70 
71 Global.players[local_id] = {
72 "character": -1,
73 "name": player_name,
74 "score": 0
75 }
76 
77 send_player_to_server.rpc_id(1, Global.players[local_id])
Singletons/Global.gd77:1   GDScript
who am I? get_unique_id() : your own peer id for this session. Every peer has a different answer.
the bridge Default name = the peer id as a string. On Steam, swap in the persona name. Same key, better label.
rpc_id(1, ...) Target peer 1: the server, always. No lookup needed.

##Who called this RPC?

Global.gd×
Global.gd > send_player_to_server
80@rpc("any_peer", "call_remote", "reliable")
81func send_player_to_server(player: Dictionary) -> void:
82 if multiplayer.is_server():
83 print("SERVER RECEIVED PLAYER DATA")
84 var sender_id: int = multiplayer.get_remote_sender_id()
85 players[sender_id] = player
86 
87 _sync_players_to_peer.rpc_id(sender_id, players)
Singletons/Global.gd87:1   GDScript
caller id get_remote_sender_id() : the peer id of whoever invoked this RPC. Never trust arguments for identity, ask the transport.
reply to sender rpc_id(sender_id, ...) answers exactly one peer. Peer ids are how RPCs are addressed.

##The bridge, in data

Global.gd×
Global.gd > add_local_player
167# Add local player to the players dictionary (call this after creating server/client)
168func add_local_player() -> void:
169 var local_id: int = multiplayer.get_unique_id()
170 print("GLOBAL ADD LOCAL PLAYER: %d" % local_id)
171 
172 # Get player name from Steam if available
173 var player_name: String = str(local_id)
174 if SteamInit.steam_running and multiplayer.multiplayer_peer is SteamMultiplayerPeer:
175 player_name = Steam.getPersonaName()
176 
177 if not players.has(local_id):
178 players[local_id] = {
179 "character": -1,
180 "name": player_name,
181 "score": 0
182 }
183 
184 # Broadcast our name to all clients
185 set_player_name.rpc(local_id, player_name)
186 
187 # Server doesn't need to wait for sync, emit immediately
188 if multiplayer.is_server():
189 players_synced.emit()
Singletons/Global.gd189:1   GDScript
key The players Dictionary is keyed by peer id. Gameplay speaks peer id.
value The name comes from Steam. Identity speaks steam id (via persona).
broadcast .rpc() with no id: tell everyone. The label follows the player to every screen.

##Who does the mapping?

peer ids
1 (host)
245023941
918234776
SteamMultiplayerPeer
owns the mapping
so you don't have to
⇄
get_steam_id_for_peer_id() →
← get_peer_id_for_steam_id()
steam ids
76561197984176210
76561198045112883
76561199103377241
Your RPCs speak peer id. Steam delivers the packets by steam id. Cooties keeps zero mapping code.
mark@laptop ~/Source/godotcon26 $ cd rpcs/
PART THREE
Talking to
each other
Remote Procedure Calls: politely yelling functions across the internet

##Anatomy of an @rpc

@rpc("authority", "call_local", "reliable")
func _start_game() -> void:
Global.change_level("res://Scenes/Game/game.tscn")

who calls it

  • "authority" : the peer that owns the node
  • "any_peer" : anyone. Validate everything.

where it runs

  • "call_local" : on you AND everyone else
  • "call_remote" : only on the other peers

how it sends

  • "reliable" : guaranteed, ordered, slower
  • "unreliable" : fast. Might not arrive. YOLO.
  • "unreliable_ordered" : drops happen, never out of order

##Certified mail vs confetti cannon

✉️ reliable

  • Like a delivery that requires a signature
  • Infections, scores, round changes
  • Anything the game breaks without
  • TCP energy (but it's all UDP underneath)

🎉 unreliable

  • A delivery person having a bad day
  • Positions (the Synchronizer's whole job)
  • Particles, sounds, cosmetic events
  • If one goes missing, another is behind it
Rule of thumb: state = reliable, streams = unreliable

##The ready-up RPCs

character_select.gd×
character_select.gd > _set_ready
64# Called when ready button is toggled
65func _on_ready_button_toggled(toggled_on: bool) -> void:
66 _set_ready.rpc(toggled_on)
67 
68 
69# Updates ready state across all clients
70@rpc("any_peer", "call_local", "reliable")
71func _set_ready(player_ready: bool) -> void:
72 is_ready = player_ready
⋮# ...
79 # Notify lobby to check if all players are ready
80 var lobby: Lobby = get_tree().get_first_node_in_group("lobby")
81 if lobby:
82 lobby.check_all_ready()
Scenes/UI/character_select.gd82:1   GDScript
lobby.gd×
lobby.gd > check_all_ready
72# Checks if all players are ready and starts the game
73func check_all_ready() -> void:
74 # Only server should check and start game
75 if not multiplayer.is_server():
76 return
77 
78 var all_ready: bool = true
79 
⋮# ...
86 if all_ready and players.get_child_count() > 0:
87 print("All players ready! Starting game...")
88 _start_game.rpc()
⋮# ...
91# Starts the game on all clients
92@rpc("authority", "call_local", "reliable")
93func _start_game() -> void:
94 Global.change_level("res://Scenes/Game/game.tscn")
Scenes/Lobby/lobby.gd94:1   GDScript
raise your hand _set_ready is "any_peer": every player toggles their own state, "call_local" updates the sender's copy too.
the starting gun _start_game is "authority": anyone can say ready, only peer 1 can say go.

##Ready up!

EVERY PLAYER THE HOST (PEER 1)
1● Everyone's connected to the lobbymultiplayer.peer_connectedsignal
2→ "I'm ready!" broadcasts to every peer_set_ready.rpc(true)rpc
3● Each peer stores everyone's ready stateis_ready = player_readylocal
4← All hands up? The host fires the starting gun_start_game.rpc()rpc
Ready is any_peer. Start is authority. Nobody moves until peer 1 says go.
mark@laptop ~/Source/godotcon26 $ cd spawn_sync/
PART FOUR
Spawning &
authority
Overriding the spawn function and setting multiplayer authority

##Who owns what?

the server owns

  • Who has Cooties
  • Scores and the round timer
  • Spawning and despawning players

each player owns

  • Their position and movement
  • Their current animation and sprite flip
  • Their player name
Authority = who writes the truth
Respect my authority
RESPECT MY AUTHORITY

##The spawner, wired upMultiplayerSpawner

player_spawner.gd×
player_spawner.gd > _ready
10func _ready() -> void:
11 spawn_function = spawn_player
12 
13 multiplayer.peer_disconnected.connect(_on_peer_disconnected)
14 
15 # Only the server should spawn players
16 if multiplayer.is_server():
17 # Spawn all players (including server)
18 for peer_id: int in Global.players.keys():
19 call_deferred("spawn", peer_id)
Scenes/Game/player_spawner.gd19:1   GDScript
player_spawner.gd×
player_spawner.gd > spawn_player
22func spawn_player(peer_id: int) -> Player:
23 var player: Player = PLAYER_SCENE.instantiate()
24 var spawn_point: Marker2D = spawn_points.pop_back()
25 
26 player.set_multiplayer_authority(peer_id)
27 player.name = str(peer_id)
28 player.global_position = spawn_point.position
⋮# ...
42 player.animated_sprite_2d.sprite_frames = character_sprite
43 return player
Scenes/Game/player_spawner.gd43:1   GDScript
wire it up Assign spawn_function, then only the server calls spawn(). Defer it so the scene finishes _ready first.
authority + name set_multiplayer_authority(peer_id) and name = str(peer_id): node paths match on every client.
the big gotcha Return the node. Do NOT add_child()! The spawner adds it for you, on every peer.
BUT WAIT... WHO'S THAT?
???
THE LATE JOINER
The late joiner

##The late joiner, under the hood

THE LATE JOINER HOST + EVERYONE ELSE
1→ Join the lobbySteamInit.peer.connect_to_lobby(lobby_id)local
2● Every peer hears about the new arrivalmultiplayer.peer_connected(peer_id)signal
3→ The joiner introduces itself to the serversend_player_to_server.rpc_id(1, players[me])rpc
4← The server answers with the full roster_sync_players_to_peer.rpc_id(sender_id, players)rpc
5● Roster received: now it's safe to enterplayers_synced.emit()signal
Everyone agrees on who exists before anyone spawns. Joining late is just joining.
mark@laptop ~/Source/godotcon26 $ watch -n 0.03 sync_state
PART FIVE
Staying in sync
MultiplayerSynchronizer: the node that keeps state flowing

##Sync this, not that

synchronize

  • Position & rotation changes every frame, drops don't matter
  • Animation state + sprite flip visual truth everyone needs
  • Anything continuous the stream heals itself next tick

rpc instead

  • One-shot effects particles and sounds want to fire once
  • Calculated values and UI derive locally from real state
  • Things that never change send names once, not 30 times a second
Synchronizer = streams. RPC = events. If it happens once, it's an event.

##The sync configMultiplayerSynchronizer

player.tscn :: replication
Player:position ................... Always
AnimatedSprite2D:animation ........ Always
PlayerName:text ................... On Change
AnimatedSprite2D:flip_h ........... Always
Replication panel
Always streams every tick, drops are fine :: On Change sends reliably, never use it for position

##Respect my authority

player.gd×
player.gd > _physics_process
51func _physics_process(delta: float) -> void:
52 # Only process input for the player we control
53 if not is_multiplayer_authority():
54 return
Scenes/Player/player.gd54:1   GDScript
the one line The most important gameplay line in this talk: if it's not yours, don't drive it.
Everyone else's copy of you is a puppet. The Synchronizer pulls the strings.

##The jitter problem

The network ticks at 30 Hz. Your monitor draws at 60+. Without help, remote players teleport 30 times a second.

player.gd×
player.gd > _ready
37func _ready() -> void:
38 # Set player name from Global
39 var peer_id: int = int(name)
40 if player_name:
41 player_name.text = Global.get_player_name(peer_id)
42 
43 if not is_multiplayer_authority():
44 physics_interpolation_mode = Node.PHYSICS_INTERPOLATION_MODE_ON
45 
46 # Server-only infection collision detection
47 if multiplayer.is_server() and infection_area:
48 infection_area.body_entered.connect(_on_infection_area_body_entered)
Scenes/Player/player.gd48:1   GDScript
Jitter before and after
one line Interpolation ON for every puppet, off for yourself (you're already smooth locally). The engine tweens between network positions. Free since 4.3.

##A physics confession

"Players stomping each other into the ground. Velocity multiplying until someone gets launched across the map."
- me, debugging Cooties at 2am

* Godot's built-in 2D physics = occasional cryptid behavior

* Rapier Physics: drop-in replacement, deterministic, actively maintained

* The whole migration is one project setting:

Project Settings > Physics > 2D > Physics Engine: Rapier2D

##One jump, 100 milliseconds

YOUR MACHINE THE SERVER + EVERYONE
1● You press jump. You move instantly. Feels great.is_multiplayer_authority() == truelocal
2→ The Synchronizer sends your position, 30 Hz.:position :: Alwayssync
3● On the server: collision! The referee makes the call_on_infection_area_body_entered(body)signal
4← Every screen agrees_set_player_infected.rpc(peer_id, true)rpc
Input to infection: about 100 ms. 50 ms there, 1 ms of server logic, 50 ms back.

##Cooties Takeaway :: five things to take home

  1. multiplayer.multiplayer_peerone property, any backend: ENet, Steam, whatever
  2. @rpc(who, where, how)annotations carry the whole messaging story
  3. MultiplayerSpawnerspawn_function + server-only spawning, return the node
  4. MultiplayerSynchronizerstreams sync, events RPC
  5. is_multiplayer_authority()guard your input, interpolate your puppets

##Shoutouts

cat shoutouts.md
$ cat shoutouts.md
my wife :: Carina for all her love and patience with my game development ❤️
built with :: reveal.js + gruvbox + JetBrains Mono (one .html file, fully offline)
community :: the Godot Discord server
the crew :: Nerdiful · friartruck · swaan · booch · ravier · OPNewPlayer
and :: everyone who contributes to Godot and its documentation
you :: everyone who attended GodotCon 💙
$
fish /home/mark/Source/godotcon26
mark@laptop ~/Source/godotcon26 $ echo $SHOUTOUT
GO CATCH COOTIES
Learn from the repo. Add it to your project. Tell me about it!
mark@laptop ~/Source/godotcon26 $
← back · advance → · ESC overview · F fullscreen