Handling Disconnects and Reconnecting
Summary
Connections drop. A player walks into a lift, a laptop sleeps, a router reboots. Reactor tells you when it happens and then waits for you to decide what to do, because only your game knows whether a dropped player should be held for thirty seconds or removed at once.
This tutorial covers detecting a disconnect, reconnecting with sensible backoff, recognising a player who comes back, and cleaning up correctly on the server when one does not.
Requirements
Completion of Running Rooms Locally and Online
Authentication if you want returning players recognised
What Reactor Does and Does Not Do
Reactor detects the drop, raises OnDisconnect, and destroys the player's server side player object. It does not retry, and it does not preserve anything for you.
The consequence that surprises people is on the server. A reconnecting player is a new player, with a new player id, not a resumed session. Reconnecting a client twice in a row produces this:
CONNECT #1 status=SUCCESS localPlayerId=3
DISCONNECT status=ABORTED
CONNECT #2 status=SUCCESS localPlayerId=4The same person on the same machine came back as player 4. Any state you keyed on player id is gone, and any entity they owned has already been dealt with under the rules below. Preserving a session is something you build, and the rest of this page is how.
Detecting a Disconnect
ksConnect exposes OnConnect, OnDisconnect and OnGetRooms as UnityEvent fields, so you can wire them in the inspector or in code. The disconnect event carries the room and a status.
The status will not tell you whether the player meant it
This is the trap. Calling Disconnect yourself raises OnDisconnect with status ABORTED, and so do several genuine failures. There is no status that means "the local side asked for this".
A reconnect handler that keys off the status alone will therefore reconnect a player who just pressed Quit. Set a flag before you disconnect deliberately, as m_leaving does above, and check it first.
The full set of statuses is SUCCESS, TIMEOUT, ABORTED, REFUSED, RESET, UNSUPPORTED_PROTOCOL, ROOM_INITIALIZE_ERROR, INVALID_ADDRESS, AUTH_ERR_USER_DEFINED, AUTH_ERR_INVALID_PROTOCOL, AUTH_ERR_CONNECTION_LIMIT, AUTH_ERR_TIMEOUT, AUTH_ERR_UNKNOWN, GET_ROOMS_ERROR and UNKNOWN_ERROR.
Worth separating: the AUTH_ERR_* group and REFUSED usually mean retrying will fail the same way, so surface them to the player instead of looping. TIMEOUT and RESET are the ones worth retrying.
ksConnect.Roomis not a reliable handle on the connected room. It can be null while the room is fully connected, so a reconnect routine that reads it will stall waiting for a value that never arrives. Use theRoomcarried on theConnectEventandDisconnectEventarguments, and keep your own reference.
Watching the Connection State
For UI, the room raises OnStateChange as it moves through NOT_CONNECTED, CONNECTING, HANDSHAKE, VALIDATING_MODEL, AUTHENTICATING, CONNECTED, DISCONNECTING, DISCONNECTED and ABORTING.
IsConnecting, IsConnected and IsDisconnecting cover the common checks without matching on the enum.
What Happens on the Server
When a client drops, the server destroys its player object and raises Room.OnPlayerLeave, and ksIServerPlayer.OnLeave on the player itself. Entities the player owned are handled by DestroyOnOwnerDisconnect on ksEntityComponent: destroyed when it is checked, left in place with no owner when it is not.
Two things about OnPlayerLeave catch people out.
Ownership is already cleared. By the time your handler runs, Entity.Owner is null. Cleanup written as if (Entity.Owner == player) never runs, and fails silently rather than erroring. Clear your own state unconditionally instead.
Update handlers can still run against a destroyed entity. A script that writes properties every frame will write one more time after its entity is gone, producing:
Reactor refuses the write and continues, so it is noise rather than breakage, but it fires on every disconnect. Return early on Entity.IsDestroyed in any handler that writes properties.
Recognising a Returning Player
Player id is assigned per connection and changes on reconnect, so it cannot identify a person. The identity that survives is ksIServerPlayer.AuthenticatedId, which is populated by authentication.
A project with no authentication has no stable identity to key on, and cannot tell a returning player from a new one. If you want sessions to survive a drop, authentication is a prerequisite rather than an optional extra. See Authentication.
With it, the pattern is to park state on leave and adopt it on join:
Deciding What to Preserve
Parking state and destroying the entity, as above, is the safer default. It frees the physics and sync cost immediately, and it cannot leave an unowned body standing in the world being shot at.
Keeping the entity alive instead, by unchecking Destroy On Owner Disconnect, suits games where the body should remain, such as a vehicle a player was driving or a character that should stay vulnerable while they scramble to reconnect. If you do that, decide explicitly what happens when the hold expires and nobody has come back, or you will accumulate abandoned entities.
Either way, set the hold to something you can defend. Long holds keep a slot occupied against players who are trying to join.
Player.IsIdleandIdleTimereport a connection that is still open but has gone quiet, which is a different condition from a disconnect and often the earlier signal. See the Idle Connections example.
Testing It
A local server will not drop you on its own, so force the cases:
Call
Disconnecton the room to test the deliberate path and confirm your intent flag stops the reconnect loop.Stop the local server while a client is connected to test an unexpected drop, then start it again and confirm the client comes back.
Watch
KinematicSoup/Reactor/logswhile you do it. The server recordsClient(id = N, address = ...) disconnected: 2-End of filefollowed by the new connection and its new id, which is the clearest confirmation that the returning player is a different player as far as the server is concerned.
Common Problems
Quitting reconnects the player straight back in
The handler treated ABORTED as a failure. A deliberate disconnect reports the same status as several real failures, so track intent with your own flag.
Reconnect routine hangs forever, never retrying
It is waiting on ksConnect.Room, which can be null while connected. Use the room from the connect and disconnect event arguments.
A returning player is treated as new
Player id changes on reconnect. Key on AuthenticatedId, which requires authentication.
Disconnect cleanup never runs
It was gated on Entity.Owner == player. Ownership is already cleared when OnPlayerLeave runs.
"Cannot set property N on destroyed entity" on every disconnect
An update handler wrote a property after its entity was destroyed. Return early on Entity.IsDestroyed.
Reconnect storms after a server restart
Every client retried on the same schedule. Back off exponentially and cap the attempts.
Memory grows in long lived rooms
Parked session state has no expiry.
Where to Go Next
Authentication: where
AuthenticatedIdcomes from.Client Connection Process: connect modes, protocols and the full status list.
Idle Connections: connections that are open but silent.
Shared Entities and Ownership Transfer: what happens to a shared object when its driver drops.
Last updated

