Skip to content

jengine-online

A game’s side of jengine-server: sign in, join rooms, and show synced entities.

KindProject package: linked into the builds of the projects that list it
Sourcepackages/jengine-online
Version0.1.0
Entry pointjengine_online::OnlinePackage
Script namespaceOnline
Depends onjengine-net
Listed byprojects/car-classics, projects/common-room, projects/cozy-corner, projects/engine-test-lab, projects/game-night, projects/hearthwood, projects/kotodama, projects/lumeria, projects/rust-bowl, projects/saltwind, projects/shooter-classics, projects/undermere

List it in the top-level "packages" array of project.jengine, together with every package it depends on: a crate dependency compiles a package in but does not register it, and packages_a_project_lists_cover_their_dependencies fails a project that lists only the outer one:

{
"packages": [
{ "name": "jengine-online", "source": { "type": "path", "path": "../../packages/jengine-online" } },
{ "name": "jengine-net", "source": { "type": "path", "path": "../../packages/jengine-net" } }
]
}

Then rebuild the editor viewport for the project (jengine viewport-build projects/<name>, or the editor’s Rebuild viewport banner). Native and web builds link it on their own.

A scene that uses its components must be a v2 scene ("version": 2): a v1 scene has no field for a package component and keeps it as inert custom data.

A script can check that the package is linked with if Online == nil then return end.

Scene keyRust type
onlineOnlinepackage only: needs a v2 scene
online_slotOnlineSlotpackage only: needs a v2 scene

Commands run in every runtime and return nothing; a package publishes what scripts read onto components (see the notes below). The flat name also works.

CallFlat name
Online.connect(e, server, name, token)online_connect
Online.join(e, kind, room, options)online_join
Online.send(e, body)online_send
Online.subscribe(e, channel)online_subscribe
Online.unsubscribe(e, channel)online_unsubscribe
Online.say(e, channel, body)online_say
Online.account(e, request)online_account
Online.input(e, x, y)online_input
Online.leave(e)online_leave
Online.disconnect(e)online_disconnect
  • projects/car-classics/assets/scenes/micro_online.scene
  • projects/car-classics/assets/scenes/rally_online.scene
  • projects/car-classics/assets/scenes/soccer_online.scene
  • projects/common-room/assets/scenes/arena.scene
  • projects/common-room/assets/scenes/defuse.scene
  • projects/common-room/assets/scenes/doodle.scene
  • projects/common-room/assets/scenes/dots.scene
  • projects/common-room/assets/scenes/eights.scene
  • projects/common-room/assets/scenes/hub.scene
  • projects/common-room/assets/scenes/krill.scene
  • projects/common-room/assets/scenes/mud.scene
  • projects/common-room/assets/scenes/wolves.scene
  • …and 27 more

From packages/jengine-online/README.md.

A game’s side of jengine-server: sign in, join a room, and see what the server says is near you as entities in the scene.

The server keeps a world and tells each player what is in view. This package is the other half: it speaks the protocol, keeps the connection alive, and moves scene entities to where the server’s entities are - smoothly, between the server’s ticks.

It needs jengine-net beside it (that package moves the bytes; this one never links it). List both in project.jengine:

"packages": [
{ "name": "jengine-net", "source": { "type": "path", "path": "../../packages/jengine-net" } },
{ "name": "jengine-online", "source": { "type": "path", "path": "../../packages/jengine-online" } }
]

and rebuild the project’s viewport. Its components need a v2 scene.

One entity is the connection:

"online": { "server": "ws://127.0.0.1:9001", "name": "Ada", "kind": "zone", "room": "meadow" }
FieldMeaning
serverthe server’s address
namethe name to sign in with
tokenwho the player is, from an earlier session; empty makes a new player
kind, rooma room to join as soon as signed in; empty kind joins nothing
auto_connectconnect when the scene starts (default true)
smoothingseconds a position takes to be walked to; 0 is one server tick, measured
retry_secsseconds before a dropped connection is tried again (default 3; 0 never)

And a pool of slots - the entities the server’s entities are shown with. A slot is any entity you like (a sprite, children, a script) saying which kind it shows:

{ "name": "Player 1", "components": { "online_slot": { "kind": "player" }, "sprite": { } } }
{ "name": "Coin 1", "components": { "online_slot": { "kind": "coin" }, "sprite": { } } }

When the server says a player came into view, a free player slot is bound to it: moved to its position every frame and given a net_entity bag with the server’s properties for it. When it leaves view the slot is freed. An unbound slot is scaled to nothing, children and all. A slot with an empty kind takes any kind that has no slot of its own left.

The pool has a size: an entity with no free slot is not shown, and overflow says how many.

A scene with two connections (a split screen, two accounts, two servers) gives each slot the session it shows for - the name of its online entity ("online_slot": { "kind": "player", "session": "Player Two" }). A slot naming none shows for the first online entity, which is every slot in a scene with one.

local conn = Entity.find_by_name("Online")
function update(dt)
local online = Entity.get_custom(conn, "online", "status")
if online == "joined" and Input.is_mouse_just_pressed() then
local x, y = Input.get_mouse_world_position()
Online.send(conn, { go = { x, y } }) -- a table; the room decides what it means
end
for _, event in ipairs(Entity.get_custom(conn, "online", "events") or {}) do
if event.t == "coins" then show_coins(event.coins) end
end
end
CallDoes
Online.connect(e, server, name, token)connect and sign in; empty arguments keep what the component says
Online.join(e, kind, room, options)join a room, now or as soon as signed in; empty room asks for any
Online.send(e, body)a message to the room
Online.input(e, x, y)steer the player’s own avatar (each -1..1; 0, 0 stops), predicted: it moves at once and is reconciled with the server
Online.leave(e)come out of the room
Online.subscribe(e, channel) / Online.unsubscribe(e, channel)join or leave a channel: chat beside the room, kept from room to room
Online.account(e, request)on a server with accounts: { change_password = { old = .., new = .. } } or { sign_out_everywhere = true }. The answer is an account event; the package keeps the new token
Online.say(e, channel, body)speak in it; everyone in it gets a said event (channel, from, name, body)
Online.disconnect(e)close the connection and stay offline

The online bag on the connection’s entity:

Field
statusoffline, signing_in, signed_in, joining, joined
player, namewho the server says we are
tokensave it - presenting it next time is what makes it the same player. Save.set("online", "token", event.token) on the welcome event, and pass it to Online.connect next launch; Test Lab scene 221 does
statethe player’s saved state, as of signing in
room{ kind, id } of the room we are in
youthe id of our own entity, if the room says ("you" on its joined frame)
joinedthe joined frame, for whatever the room put on it
errorthe last refusal
count, overflowentities in view, and how many of them have no slot
eventsevery frame the server sent this frame except sync, as tables

On each bound slot, the net_entity bag: id, kind, mine (it is our own entity), and whatever the room set on it - name, hp.

Online.send(e, { go = { x, y } }) asks the server to walk the avatar, and it moves when the server moves it: a round trip and a tick later. For direct control use Online.input(e, x, y) instead. The package sends one numbered input per fixed step and applies it to the player’s own slot at once, with the same step function the server runs (jengine-netcode, linked by both ends; the room sends its numbers on joined). When the server answers it starts from there and replays what the server has not seen - which, with a shared step function, changes nothing. If the server knew better (a wall, a shove), the difference is eased out rather than snapped.

local x = (Input.is_key_pressed("D") and 1 or 0) - (Input.is_key_pressed("A") and 1 or 0)
local y = (Input.is_key_pressed("W") and 1 or 0) - (Input.is_key_pressed("S") and 1 or 0)
Online.input(conn, x, y)

The player’s own slot gets predicted, server_x and server_y in its net_entity bag; the online bag gets predicting, unacknowledged and the server’s tick. Everyone else is drawn as before, a tick in the past. It needs a room that takes input messages and says "netcode" on joined

  • the server’s zones example and its world template do. A room with walls sends them too (netcode.walls, a grid of solid cells) and the prediction is stopped by the same walls the server stops it with - the zones example builds them from an engine Tilemap its monsters path through.

The package reconnects on its own, signs in with the token it was given, and goes back to the room it was in. If that kind of room holds seats (grace on the server) and it is back in time, nobody saw it go: same avatar, same place. Otherwise the others see the player leave and return - still the same player, with what they had. The channels it was in are joined again by themselves.

Replaced is not dropped. When the same player signs in somewhere else - another browser tab (tabs share a save, and so the token), or a second copy run without --instances - the server lets the older connection go with { "t": "error", "why": "signed in somewhere else" }. The package then stays off and raises { "t": "replaced" } instead of dropped: coming back would push the other window off, and the two used to trade the player back and forth, each showing “Connection lost” while it played on. Connect again to take the player back.

Signing in again - Online.disconnect(e) then Online.connect(e, ..) in the same frame, to change the name or the server - opens a new socket: whatever follows a disconnect waits for the next frame, by which time the old socket is closed. (It used to share the old socket and send a second hello down it, which the server refuses: “already signed in”.)

  • Spawn anything. It binds slots the scene already has. That is what makes it the same in every runtime; the price is a pool with a size.
  • Predict anything but movement. The player’s own avatar is predicted when steered with Online.input; a shot, a pickup or a spawn is not - it happens when the server says. And the one movement rule built in is top-down (a direction, a speed, a rectangle).
  • More than one connection. One online entity owns the slots.

projects/engine-test-lab scene 221 is the worked example. It has been run in tests (two hosted clients against a real server) and on screen in the native runtime; not yet in the editor or a web build.