jengine-online
A game’s side of jengine-server: sign in, join rooms, and show synced entities.
| Kind | Project package: linked into the builds of the projects that list it |
| Source | packages/jengine-online |
| Version | 0.1.0 |
| Entry point | jengine_online::OnlinePackage |
| Script namespace | Online |
| Depends on | jengine-net |
| Listed by | projects/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 |
Using it
Section titled “Using it”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.
Components
Section titled “Components”| Scene key | Rust type | |
|---|---|---|
online | Online | package only: needs a v2 scene |
online_slot | OnlineSlot | package only: needs a v2 scene |
Script commands
Section titled “Script commands”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.
| Call | Flat 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 |
Scenes that use it
Section titled “Scenes that use it”projects/car-classics/assets/scenes/micro_online.sceneprojects/car-classics/assets/scenes/rally_online.sceneprojects/car-classics/assets/scenes/soccer_online.sceneprojects/common-room/assets/scenes/arena.sceneprojects/common-room/assets/scenes/defuse.sceneprojects/common-room/assets/scenes/doodle.sceneprojects/common-room/assets/scenes/dots.sceneprojects/common-room/assets/scenes/eights.sceneprojects/common-room/assets/scenes/hub.sceneprojects/common-room/assets/scenes/krill.sceneprojects/common-room/assets/scenes/mud.sceneprojects/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.
Opting in
Section titled “Opting in”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.
What you author
Section titled “What you author”One entity is the connection:
"online": { "server": "ws://127.0.0.1:9001", "name": "Ada", "kind": "zone", "room": "meadow" }| Field | Meaning |
|---|---|
server | the server’s address |
name | the name to sign in with |
token | who the player is, from an earlier session; empty makes a new player |
kind, room | a room to join as soon as signed in; empty kind joins nothing |
auto_connect | connect when the scene starts (default true) |
smoothing | seconds a position takes to be walked to; 0 is one server tick, measured |
retry_secs | seconds 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.
From a script
Section titled “From a script”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 endend| Call | Does |
|---|---|
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 | |
|---|---|
status | offline, signing_in, signed_in, joining, joined |
player, name | who the server says we are |
token | save 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 |
state | the player’s saved state, as of signing in |
room | { kind, id } of the room we are in |
you | the id of our own entity, if the room says ("you" on its joined frame) |
joined | the joined frame, for whatever the room put on it |
error | the last refusal |
count, overflow | entities in view, and how many of them have no slot |
events | every 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.
Steering, predicted
Section titled “Steering, predicted”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
worldtemplate 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 engineTilemapits monsters path through.
A dropped connection
Section titled “A dropped connection”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”.)
What it does not do
Section titled “What it does not do”- 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
onlineentity 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.