Bevy tracking crates.io

Bevy Quinnet

A Client/Server game networking plugin using QUIC, for the Bevy game engine.

QUIC as a game networking protocol

QUIC was really attractive to me as a game networking protocol because most of the hard-work is done by the protocol specification and the implementation (here Quinn). No need to reinvent the wheel once again on error-prones subjects such as a UDP reliability wrapper, some encryption & authentication mechanisms, congestion-control, and so on.

Most of the features proposed by the big networking libs are supported by default through QUIC. As an example, here is the list of features presented in GameNetworkingSockets:

-> Roughly 9 points out of 11 by default.

(*) Kinda, when sharing a QUIC stream, reliable messages need to be framed.

Features

Quinnet has basic features, I made it mostly to satisfy my own needs for my own game projects.

It currently features:

Although Quinn and parts of Quinnet are asynchronous, the APIs exposed by Quinnet for the client and server are synchronous. This makes the surface API easy to work with and adapted to a Bevy usage. The implementation uses tokio channels internally to communicate with the networking async tasks.

Roadmap

This is a bird-eye view of the features/tasks that will probably be worked on next (in no particular order):

Quickstart

Client

rust App::new() // ... .add_plugin(QuinnetClientPlugin::default()) // ... .run();

```rust fn startconnection(client: ResMut) { client .openconnection( ClientConfigurationData::from_ips( IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 6000, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0, ), CertificateVerificationMode::SkipVerification, );

// When trully connected, you will receive a ConnectionEvent

```

rust fn handle_server_messages( mut client: ResMut<Client>, /*...*/ ) { while let Ok(Some(message)) = client.connection().receive_message::<ServerMessage>() { match message { // Match on your own message types ... ServerMessage::ClientConnected { client_id, username} => {/*...*/} ServerMessage::ClientDisconnected { client_id } => {/*...*/} ServerMessage::ChatMessage { client_id, message } => {/*...*/} } } }

Server

rust App::new() /*...*/ .add_plugin(QuinnetServerPlugin::default()) /*...*/ .run();

rust fn start_listening(mut server: ResMut<Server>) { server .start_endpoint( ServerConfigurationData::from_ip(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 6000), CertificateRetrievalMode::GenerateSelfSigned, ) .unwrap(); }

rust fn handle_client_messages( mut server: ResMut<Server>, /*...*/ ) { let mut endpoint = server.endpoint_mut(); for client_id in endpoint.clients() { while let Some(message) = endpoint.try_receive_message_from::<ClientMessage>(client_id) { match message { // Match on your own message types ... ClientMessage::Join { username} => { // Send a messsage to 1 client endpoint.send_message(client_id, ServerMessage::InitClient {/*...*/}).unwrap(); /*...*/ } ClientMessage::Disconnect { } => { // Disconnect a client endpoint.disconnect_client(client_id); /*...*/ } ClientMessage::ChatMessage { message } => { // Send a message to a group of clients endpoint.send_group_message( client_group, // Iterator of ClientId ServerMessage::ChatMessage {/*...*/} ) .unwrap(); /*...*/ } } } } }

You can also use endpoint.broadcast_message, which will send a message to all connected clients. "Connected" here means connected to the server plugin, which happens before your own app handshakes/verifications if you have any. Use send_group_message if you want to control the recipients.

Channels

There are currently 3 types of channels available when you send a message: - OrderedReliable: ensure that messages sent are delivered, and are processed by the receiving end in the same order as they were sent (exemple usage: chat messages) - UnorderedReliable: ensure that messages sent are delivered, in any order (exemple usage: an animation trigger) - Unreliable: no guarantees on the delivery or the order of processing by the receiving end (exemple usage: an entity position sent every ticks)

By default for the server as well as the client, Quinnet creates 1 channel instance of each type, each with their own ChannelId. Among those, there is a default channel which will be used when you don't specify the channel. At startup, this default channel is an OrderedReliable channel.

rust let connection = client.connection(); // No channel specified, default channel is used connection.send_message(message); // Specifying the channel id connection.send_message_on(ChannelId::UnorderedReliable, message); // Changing the default channel connection.set_default_channel(ChannelId::Unreliable);

One channel instance is more than enough for UnorderedReliable and Unreliable since messages are not ordered on those, in fact even if you tried to create more, Quinnet would just reuse the existing ones. This is why you can directly use their ChannelId when sending messages, as seen above.

In some cases, you may however want to create more than one channel instance, it may be the case for OrderedReliable channels to avoid some Head of line blocking issues. Channels can be opened & closed at any time.

rust // If you want to create more channels let chat_channel = client.connection().open_channel(ChannelType::OrderedReliable).unwrap(); client.connection().send_message_on(chat_channel, chat_message);

On the server, channels are created and closed at the endpoint level and exist for all current & future clients. rust let chat_channel = server.endpoint().open_channel(ChannelType::OrderedReliable).unwrap(); server.endpoint().send_message_on(client_id, chat_channel, chat_message);

Certificates and server authentication

Bevy Quinnet (through Quinn & QUIC) uses TLS 1.3 for authentication, the server needs to provide the client with a certificate confirming its identity, and the client must be configured to trust the certificates it receives from the server.

Here are the current options available to the server and client plugins for the server authentication: - Client : - [x] Skip certificate verification (messages are still encrypted, but the server is not authentified) - [x] Accept certificates issued by a Certificate Authority (implemented in Quinn, using rustls) - [x] [Trust on first use](https://en.wikipedia.org/wiki/Trustonfirst_use) certificates (implemented in Quinnet, using rustls) - Server: - [x] Generate and issue a self-signed certificate - [x] Issue an already existing certificate (CA or self-signed)

On the client:

rust // To accept any certificate client.open_connection(/*...*/, CertificateVerificationMode::SkipVerification); // To only accept certificates issued by a Certificate Authority client.open_connection(/*...*/, CertificateVerificationMode::SignedByCertificateAuthority); // To use the default configuration of the Trust on first use authentication scheme client.open_connection(/*...*/, CertificateVerificationMode::TrustOnFirstUse(TrustOnFirstUseConfig { // You can configure TrustOnFirstUse through the TrustOnFirstUseConfig: // Provide your own fingerprint store variable/file, // or configure the actions to apply for each possible certificate verification status. ..Default::default() }), );

On the server:

rust // To generate a new self-signed certificate on each startup server.start_endpoint(/*...*/, CertificateRetrievalMode::GenerateSelfSigned { server_hostname: "127.0.0.1".to_string(), }); // To load a pre-existing one from files server.start_endpoint(/*...*/, CertificateRetrievalMode::LoadFromFile { cert_file: "./certificates.pem".into(), key_file: "./privkey.pem".into(), }); // To load one from files, or to generate a new self-signed one if the files do not exist. server.start_endpoint(/*...*/, CertificateRetrievalMode::LoadFromFileOrGenerateSelfSigned { cert_file: "./certificates.pem".into(), key_file: "./privkey.pem".into(), save_on_disk: true, // To persist on disk if generated server_hostname: "127.0.0.1".to_string(), });

See more about certificates in the certificates readme

Logs

For logs configuration, see the unoffical bevy cheatbook.

Examples

Chat example

This demo comes with an headless server, a terminal client and a shared protocol.

Start the server with cargo run --example chat-server and as many clients as needed with cargo run --example chat-client. Type quit to disconnect with a client.

terminal<em>chat</em>demo

Breakout versus example

This demo is a modification of the classic Bevy breakout example to turn it into a 2 players versus game.

It hosts a local server from inside a client, instead of a dedicated headless server as in the chat demo. You can find a server module, a client module, a shared protocol and the bevy app schedule.

It also makes uses of Channels. The server broadcasts the paddle position every tick via the PaddleMoved message on an Unreliable channel, the BrickDestroyed and BallCollided events are emitted on an UnorderedReliable channel, while the game setup and start are using the default OrdrerdReliable channel.

Start two clients with cargo run --example breakout, "Host" on one and "Join" on the other.

breakoutversusdemo_short.mp4

Examples can be found in the examples directory.

Compatible Bevy versions

Compatibility of bevy_quinnet versions:

| bevy_quinnet | bevy | | :------------- | :----- | | 0.4 | 0.10 | | 0.2-0.3 | 0.9 | | 0.1 | 0.8 |

Limitations

Credits

Thanks to the Renet crate for the inspiration on the high level API.

License

bevy-quinnet is free and open source! All code in this repository is dual-licensed under either:

at your option. This means you can select the license you prefer! This dual-licensing approach is the de-facto standard in the Rust ecosystem and there are very good reasons to include both.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.