2 releases

0.1.1 Oct 6, 2024
0.1.0 Oct 6, 2024

#647 in Game dev

44 downloads per month

MIT license

34KB
631 lines

Bevy crab networking

Multiplayer made easy! ...easier.

This is a Bevy networking plugin based on TCP. It makes hosting a server, connecting to it, and sending packets of data to and from as easy as pie! No wait, that's Python. Crabcake? Now that you know that this Readme and the crate weren't written by ChatGPT, let's get down to business.

If you don't like reading my incoherent ramblings, here are some examples

Setup

To setup the plugin, add the BevyNetworkingPlugin to your plugins and if you're making a server, add the ServerConfig resource, and if you're making a client, add the ClientConfig resource. Keep in mind that it is highly advised that both your client and server depend on a single library to avoid confusion and duplicate code.

Client

fn main() {
    App::new()
        .add_plugins((DefaultPlugins, BevyCrabNetworkingPlugin))
        .insert_resource(ClientConfig {
            server_address: "127.0.0.1:46393".parse().unwrap(),
            auto_reconnect: AutoReconnect::Auto {
                reconnection_time: 5.,
            },
        })
        .run();
}

Server

fn main() {
    App::new()
        .add_plugins((MinimalPlugins, BevyCrabNetworkingPlugin))
        .insert_resource(ServerConfig { host_port: 46393 })
        .run();
}

What is AutoReconnect? Well, if the client gets disconnected from the server for any reason, or never even manages to connect in the first place, it will try again and again, until it succeeds. You can disable this whenever you like with AutoReconnect::None

Sending Data

lib.rs

Create a struct or enum that implements the traits Identify, Serialize, Deserialze and Debug. Identify is provided by the crate and is used to "tag" the packet with an identifier, so that you can match with it later

#[derive(Serialize, Deserialize, Debug)]
pub enum Packet {
    Message(String),
}
impl Identify for Packet {
    fn get_identifier(&self) -> u32 {
        0
    }
}

client.rs

You can use ClientDataUploader to upload things from the client to the server. This will return an error if the client is not connected to the server, so in order to prevent panics, you can use the is_connected function to check if it is connected to the server, or you can run the function only when is_connected_to_server returns true

fn send_messages(mut client_data_uploader: ResMut<ClientDataUploader>) {
    if client_data_uploader.is_connected() {
        if let Err(err) = client_data_uploader.upload(Packet::Message("hello".into())) {
            eprintln!("Failed to send packet: {err:?}");
        }
    }
}
fn main() {
    App::new()
        .add_systems(Update, send_messages.run_if(is_connected_to_server))
        .run();
}

server.rs

Similarly, you can use ServerDataUploader to upload things from the server to the client. This time, you need to specify a Recipient, which can be All, AllExcept {id : u32} or Single {id : u32}. The recipient specifies to what clients the Packet will be sent.

fn send_messages(mut data_uploader: ResMut<ServerDataUploader>) {
    data_uploader.upload(
        Packet::Message(format!("Hello to you too!",)),
        Recipient::All,
    );
}

There is no need to check anything this time.

Receiving Data

client.rs

Now that identifier that we specified with the Identify trait will come in handy! You match the struct or enum with the identifier in the DataPacket from the ClientDataReadEvent, and then in the case of the Packet enum you created, you can match with that to see what kind of data you received.

fn handle_incoming_data(mut client_data_reader: EventReader<ClientDataReadEvent>) {
    for event in client_data_reader.read() {
        match event.data_packet.identifier {
            0 => match bincode::deserialize::<Packet>(&event.data_packet.bytes)
                .expect("Failed to deserialize packet")
            {
                Packet::Message(message) => {
                    println!("Received message: {}", message);
                }
            },
            _ => {}
        }
    }
}

server.rs

Instead of a ClientDataReadEvent, you get a ServerDataReadEvent. The only difference is, that in addition to the DataPacket, you also get an id: u32 that you can use as an id to tell players apart. That is the id of the player that sent you the Packet. You can also, for example, use it with the Recipient in ServerDataUploader to forward a message to every player except the one that sent you it.

fn handle_incoming_data(
    mut server_data_reader: EventReader<ServerDataReadEvent>,
    mut data_uploader: ResMut<ServerDataUploader>,
) {
    for event in server_data_reader.read() {
        match event.data_packet.identifier {
            0 => {
                match bincode::deserialize::<Packet>(&event.data_packet.bytes)
                    .expect("Failed to deserialize packet")
                {
                    Packet::Message(message) => {
                        println!(
                            " Received a message from id: {}. Message: {}",
                            event.id,
                            message.clone()
                        );
                        data_uploader.upload(
                            Packet::Message(message),
                            Recipient::AllExcept { id: event.id },
                        );
                    }
                }
            }
            _ => {}
        }
    }
}

Compatible Bevy versions

Bevy version bevy_crab_networking version
0.14 0.1.0

License

Licensed under the MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)

Dependencies

~22–35MB
~550K SLoC