Skip to content

Conversation

@cb341
Copy link
Owner

@cb341 cb341 commented Jan 25, 2026

Introduce Usernames

CleanShot X 2026-01-25 13 54 51 CleanShot X 2026-01-26 01 51 08

Player usernames and better state management.

  • CLI arg parsing for username parameter on client start
  • One client per username
  • Username display entities
  • Refactoring of ClientId to Username in Chat and other constructs
  • Position, Rotation restoring when reconnecting to Server
  • Improved handling of Client/Server disconnects

This feature was suprisingly time consuming to implement as there were many bugs with the old implementation of client/server networking.

Impl

Chat messages and player positions were originally identified by unique client ids.
The ClientIds are generated on the start of the client binary and are randomly generated.
When the client disconnected from the server, the player state (position,rotation) would be dropped as the client cannot reconnect with the same unique id.

Usernames on the other hand, allow us to store player state and have players rejoin and keep their state.

We cannot construct ClientId from username hash for example, because Renet doesn't handle non-unique client connections.
https://github.com/lucaspoffo/renet/blob/631fc8addc178a8f8bbb215f19a7cc471b8abb5f/renet/src/server.rs#L35-L49

When connecting to the server we can pass additional 250 ish bytes in a user_data byte array
https://github.com/lucaspoffo/renet/blob/631fc8addc178a8f8bbb215f19a7cc471b8abb5f/demo_chat/src/main.rs#L51-L70
https://github.com/lucaspoffo/renet/blob/master/renetcode/src/client.rs#L43-L43

Imo 250 bytes for a username is too much so I capped it at 50.
In the chat demo they created a wrapper type for a String. Strings are heap allocated and cannot be easily copied. I was very annyed by the explicit clones everywhere, so I decided to represent Usernames as byte arrays 6f3f8da. In addition to being more readable it should be faster (although this has no real world impact, the code readability does).

The username should be somehow passable by the user and not be randomly generated. Two options:

  1. Add CLI argument to client binary (easy as we already have clap as a dependency)
  2. Create a custom GUI that gets rendered in the client when in a StartScreen state or something (difficult)

I decided to go with

$ car rc
error: the following required arguments were not provided:
  --username <USERNAME>

Usage: client --username <USERNAME>

For more information, try '--help'.

Then I updated all data structures that previously used ClientId as a key to use the new Username instead.
I noticed that for ChatMessage I was using 0 to represent the SERVER sender. This would be stupid with Username so I replaced it with a proper enum.

I spent quite some time digging around Renet because I had issues with clients not disconnecting properly and apparently this has been a historic issue as well:

When using CTRL+C, the app exit event is triggered, client sends disconnect message to server.
When closing a window via controls, app exit is triggered and disconnect message is sent.
But when closing a window via CMD+Q, no app exit is emmited and the server thinks the client is still connected only realizing the opposite is true after something like a 15 second interval.

It turns out that Bevy uses AppExit Errors to indicate a graceful shutdown.
When using CTRL+C you need to have the TerminalCtrlCHandlerPlugin installed.

This is usually included by the DefaultPlugins:
https://docs.rs/bevy/latest/bevy/app/struct.TerminalCtrlCHandlerPlugin.html

Then I wanted to show the text above the remote player, so I looked for crates.
This one looked like the best pick: https://github.com/kisya-games/bevy_mod_billboard.

When setting up Billboard, I struggled with the text component:
eckz/bevy_flair#38

Also: The relative text position was somehow not adjustable by adding a Transform::Translate3d to the Node bundle so I just decided to add a few blanklines.

Before:

CleanShot 2026-01-26 at 01 20 01@2x

After:

CleanShot 2026-01-26 at 01 20 39@2x

Problems with Networking.
In the flow I want the Client to ask the server for permission to join.
The server receives the ServerEvent::ClientConnected event, checks if another client has already picked the username, if so the new client get's rejected. The problem was that I wanted to have the client receive the Reject event as well as disconnect the client at the server. When disconnecting the client immediately, it wouldn't receive the Reject event and wouldn't behave properly. Thus I introduced a queue. You can queue new clients to be disconnected and you can retain ready clients. this guarantees that events such as Reject get sent properly before we terminate the connection.

I also needed a efficient DataStructure to map ClientIds to Usernames and back, so I decided to go with a bidirectional HashMap. It is a wrapper around two HashMaps. When inserting a ClientId, Username pair, I update both tables:

self.client_to_username.insert(client_id, username);
self.username_to_client.insert(username, client_id);

There is a memory leak, so if you start your client, kill it and restart it over and over again, the server allocates another 51*2 bytes every time as the ClientIdUsername mappings aren't collected.

Adding the spawn state was pretty easy as I could just delegate the spawn point to the accept event and add another resource.

Lessons

  • Reading code from Libraries is a good place to learn (& copy).
  • Types that implement the Copy trait are very nice to work with.
  • Enums are super cool
  • Try to limit scope before working on a PR. otherwise it may explode.

Other


Made with ❤️

@cb341 cb341 self-assigned this Jan 25, 2026
@cb341 cb341 changed the base branch from main to feature/persistent-world January 25, 2026 12:52
Base automatically changed from feature/persistent-world to main January 25, 2026 13:21
@cb341 cb341 marked this pull request as draft January 25, 2026 13:25
@cb341 cb341 changed the title Feature/usernames Usernames Jan 25, 2026
@cb341 cb341 force-pushed the feature/usernames branch from 72d59cf to e8ae125 Compare January 25, 2026 13:28
@cb341 cb341 force-pushed the feature/usernames branch from 6b4b938 to 1c862e5 Compare January 25, 2026 21:50
@cb341 cb341 marked this pull request as ready for review January 26, 2026 00:33
self.as_str() == SERVER_USERNAME
}

pub fn to_netcode_user_data(&self) -> [u8; NETCODE_USER_DATA_BYTES] {
Copy link
Owner Author

@cb341 cb341 Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cb341 cb341 merged commit 5d79681 into main Jan 26, 2026
3 checks passed
@cb341 cb341 deleted the feature/usernames branch January 26, 2026 10:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants