Appearance
Network sockets
A standalone build can talk to a server of your own: a multiplayer server you host, a leaderboard, a chat service, a level service, or any existing server that speaks over WebSockets. The net namespace opens the connection and carries bytes in both directions. What the bytes mean, how a login goes, when to reconnect: all of that is your script's. The engine has no opinion about it.
Where it works
| Runtime | Sockets |
|---|---|
| A standalone build (Map → Build Standalone) | Yes |
| A standalone build with Offline multiplayer room, client scripts | Yes, as in any standalone build |
| A standalone build with Offline multiplayer room, server scripts | No, as in a Galatrix room |
| The editor play-test | Yes, for listed hosts; an unlisted host asks you once per session |
| A Galatrix room, client scripts of a game published by an admin | Yes, to listed hosts only, over wss:// |
| A Galatrix room, client scripts of any other game | No |
| A Galatrix room, server scripts | No |
Where a script has no sockets, net.isAvailable() answers false and every other net call throws one message: net: sockets work in standalone builds, the editor play-test, and Galatrix rooms of games published by admins (client scripts only). A game that has an online mode hides that menu behind isAvailable() and keeps working offline.
In a Galatrix room
A room lets client scripts open sockets when the game was published by an admin. The room decides when it starts, so a room that is already running keeps its answer until it closes. A Test Online room follows whoever started it: an admin can test a game's sockets there before publishing it.
- Client scripts only. A room's server scripts never get sockets: a game server opening connections could reach services on its own network.
- Listed hosts only. A host the list does not name is refused, and "Allow any host" does nothing in a room: players never type an address there.
wss://only. The play page is served over https, so aws://address is refused, a local one too.- The player's browser makes the connection. The host sees the player's IP address and the page's origin, and whatever your script sends it.
Listing your hosts
Every host a script may connect to is listed first, under Game Settings → Network → Allowed hosts, one per line:
play.example.net # this host, any port
play.example.net:8081 # this host, only that port
*.example.net # any subdomain (not example.net itself)
localhost:8080 # a server on your own machineConnections use wss://. Plain ws:// is accepted only to localhost, 127.0.0.1 and [::1], for a server running on the same machine while you develop.
A connect to a host the list does not name throws, and the message names the setting to change. That is deliberate: a script from an imported kit, or one an assistant wrote, cannot quietly send a player's browser somewhere you never listed.
With Run scripts unguarded on, the list still governs net.connect, but it is advisory: an unguarded script can open a WebSocket of its own.
Hosts the player types
A game with a server list or a direct-connect field cannot know its hosts in advance. For that, turn on Game Settings → Network → Allow any host. In a standalone build the engine then asks the player before the first connection to an unlisted host, in a dialog no script can draw or press:
This game wants to connect to play.example.net. Allow?
A yes is remembered for that host, for that game, in that browser. A no is not remembered, so a slip of the mouse can be retried. Listed hosts never ask. The switch is off by default and the panel says why; leave it off unless the player really types the address.
In the play-test the switch does not matter: a listed host connects, and an unlisted one asks you, once per editor session. In a Galatrix room it does nothing: an unlisted host is refused.
The API
js
class Online extends Behaviour {
onStart() {
if (!this.net.isAvailable()) { this.ui.showMessage('Online play needs the downloadable build'); return }
this.sock = this.net.connect('wss://play.example.net/ws', { protocols: ['v1'] })
this.sock.onOpen = () => this.sock.send(new Uint8Array([0x01]))
this.sock.onMessage = (data) => {
if (typeof data === 'string') this.debug.log('text:', data)
else this.handlePacket(new DataView(data)) // binary frames arrive as an ArrayBuffer
}
this.sock.onError = (message) => this.debug.warn('socket error:', message)
this.sock.onClose = (code, reason, wasClean) => this.ui.showMessage(`Disconnected (${code}) ${reason}`)
}
handlePacket(view) { /* your protocol */ }
}| Member | What it does |
|---|---|
net.connect(url, { protocols }) | Opens a socket. Throws only for misuse: a runtime without sockets, a malformed URL, a scheme the rules refuse, or a host nobody allowed. A connection that fails on the network reports through onError and then onClose. |
net.isAvailable() | Whether this runtime can open sockets at all. |
net.allowedHosts() | The host list from Game Settings, for a server list screen. |
sock.onOpen | The connection is open; you can send. |
sock.onMessage(data) | One frame. An ArrayBuffer for a binary frame, a string for a text frame. |
sock.onClose(code, reason, wasClean) | The connection ended. 1000 is a normal close; the reason is what the other side sent. |
sock.onError(message) | Something went wrong. onClose follows. |
sock.send(data) | Sends a string, an ArrayBuffer, a typed array or a DataView. Throws when the socket is not open. |
sock.close(code, reason) | Closes. 1000 by default; codes 3000 to 4999 are yours to define. |
sock.readyState | 'connecting', 'open', 'closing' or 'closed'. |
sock.bufferedAmount | Bytes queued by send and not yet handed to the network. |
sock.url, sock.protocol | The URL, and the subprotocol the server chose. |
Compressed data
A server that sends its payload compressed (zlib, deflate or gzip) can be read with the browser's own DecompressionStream, and CompressionStream packs data the same way. Both are promise-based, so handle frames through one chain to keep them in order:
js
async inflate(bytes) {
const stream = new DecompressionStream('deflate') // 'deflate' is the zlib format; 'deflate-raw', 'gzip' too
const writer = stream.writable.getWriter()
writer.write(bytes)
writer.close()
const reader = stream.readable.getReader()
const parts = []
for (;;) {
const { done, value } = await reader.read()
if (done) break
parts.push(value)
}
return parts // Uint8Array pieces, in order
}What the engine promises
Frames are delivered when the browser delivers them, in the socket's order, also while the tab is hidden. Nothing is held for the next game frame, so a server that expects a keep-alive answer every few seconds gets it while the player is in another tab. Answering from inside onMessage is fine.
There is no size limit on a frame and no conversion of its contents: an ArrayBuffer reaches your handler as the browser handed it over. The 16 KB limit on script events does not apply here. A typed array you pass to send is handed to the browser as it is, without a copy, so reuse its buffer only after the call returns.
Handlers are called the way HUD button handlers are: directly. A throw inside one is logged to the Console and the socket stays open.
You can open as many sockets as the game needs. A server list that pings every entry while the game connection is open is the normal case.
When a socket closes by itself
A socket belongs to the script instance that opened it. It closes when:
- the entity is destroyed
- play stops
- another scene loads
- in a Galatrix room, the player leaves the room or moves to another of the game's maps (
players.teleportToScene)
A DontDestroyOnLoad entity keeps its sockets across a scene load, with its scripts and its HUD, so a connection manager that lives on such an entity stays connected from the lobby scene into the match. In a Galatrix room a map switch starts the client scripts over, so their sockets close and the new map's scripts connect again.
When the engine closes a socket, the script's onClose does not run: the instance it belonged to is gone.
Opening a build from disk
A standalone HTML opened straight from a folder runs on a file:// page. The browser then sends null as the connection's Origin, and a script cannot change that. Some servers refuse a null origin; if yours does, it can accept it explicitly, or the game can be served over http from a small static server.
Every connection, refusal and close is written to the Console (the browser console in a build, when Debug Mode is on), labelled with the entity that opened it.