5 releases (3 breaking)

new 0.3.0 Apr 17, 2024
0.2.0 Mar 6, 2024
0.1.1 Dec 2, 2023
0.0.2 Nov 14, 2023

#287 in Game dev

Download history 4/week @ 2024-02-16 4/week @ 2024-02-23 106/week @ 2024-03-01 30/week @ 2024-03-08 3/week @ 2024-03-15 54/week @ 2024-03-29 12/week @ 2024-04-05

70 downloads per month

MIT/Apache

84KB
2K SLoC

bevy_bundlication

Network replication for bevy based on a bundle pattern.

Goals

  • High performance replication
  • Minimizing bandwidth overhead
  • Features to synchronize predicted and remote entities
    • Rollback is out of scope
  • Support well-scoped client authority

Non-goals

  • Being the easiest crate for less performance and bandwidth critical uses

Getting started

bevy_bundlication works with a pattern similar to a Bundle from bevy. Anything matching the bundle, with the required component to get picked up by networking (Identifier) is sent according to the rules it was registered with. Each field needs to implement bevy::ecs::component::Component, serde::Serialze, serde::Deserialize and Clone (for now). The bundle also needs to derive Bundle (for now) and TypePath.

Bundles need to be registered to the app, and can have extra rules on fields.

#[derive(Component, Default)]
pub struct Player;

#[derive(Component, Serialize, Deserialize, Clone)]
pub struct Speed(f32);

#[derive(Serialize, Deserialize)]
pub struct JustTranslation(Vec3);

impl NetworkedWrapper<Transform> for JustTranslation {
    fn write_data(
        from: &Transform,
        writer: impl Write,
        tick: Tick,
        map: &IdentifierMap,
    ) -> IdentifierResult<()> {
        serialize(writer, from.translation).unwrap();
        Ok(())
    }

    fn read_new(
        reader: impl Read,
        tick: Tick,
        map: &mut IdentifierManager,
    ) -> IdentifierResult<Transform> {
        let translation: Vec3 = deserialize(reader).unwrap();
        Ok(Transform::from_translation(translation))
    }
}

#[derive(NetworkedBundle, Bundle, TypePath)]
pub struct PlayerPositionBundle {
    // This field doesn't get sent, but it will get spawned (with the default value)
    #[networked(no_send)]
    pub player: Player,
    // This components is queried and spawned as Transform, but sent according to
    // the logic of JustTranslation
    #[networked(as = JustTranslation)]
    pub translation: Transform,
    // This component is sent and spawned as is
    pub speed: Speed,
}

pub struct MovementPlugin;

impl Plugin for MovementPlugin {
    fn build(&self, app: &mut App) {
        // We register the bundle to be sent from the server to all clients.
        // These registers need to be identical on both the client and server.
        // but don't worry, the client won't try to send server data,
        // nor would the server accept it
        app.register_bundle::<ServerToAll, PlayerPositionBundle, 0>();

        // Other rules for how a bundle is sent are:
        // - ServerToOwner and ServerToObserver, which both check the Identifier (or Owner, if it exists)
        // - ClientToServer, for client authority which requires an Authority::Free or
        //      matching Authority::Client(x) on the client
    }
}

Future plans

  • More optimizations
  • Getting rid of Identifier in favor of a as-needed system to match predicted and real entities
  • Add per-entity per-bundle client authority control
  • Client-side packet buffering (to reduce jitter and support accurate interpolation)
  • Per-entity visibility control

License

All code in this repository is dual-licensed under either:

at your option.

Dependencies

~17–47MB
~734K SLoC