#bevy-ecs #ecs #bevy #save #serde

yanked bevy-salo

An ECS based serialization crate for bevy_ecs with no dependency on reflection

2 releases

0.1.1 Nov 27, 2023
0.1.0 Nov 26, 2023

#79 in #bevy-ecs

MIT/Apache

87KB
2K SLoC

Rust 1.5K SLoC // 0.0% comments Rusty Object Notation 386 SLoC

bevy-salo

Latest version Documentation MIT/Apache 2.0

bevy_salo (SAveLOad) is an ECS based serialization crate for bevy_ecs.

Unique Features

  • Not dependent on reflection or bevy_app.
  • Greater user control.
  • Match entities by name.
  • Custom ser/de methods that can load resources, spawn entities, etc.

Getting Started

To get started, register the plugin and all the types you need to serialize.

// It is recommanded to alias here.
type All = bevy_salo::All<SerdeJson>;
app.add_plugins(
    SaveLoadPlugin::new::<All>()
        .register::<Unit>()
        .register::<Weapon>()
        .register::<Stat>()
        .register::<Hp>()
);

Generic types (unforunately) need to be registered separately.

SaveLoadPlugin::new::<All>()
    .register::<Unit<Human>>()
    .register::<Unit<Monster>>()

All serializes all entities, to narrow the scope with a marker component:

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

impl bevy_salo::MarkerComponent for SaLo {
    // Set the serialization method here.
    type Method = SerdeJson;
}

app.add_plugins(
    SaveLoadPlugin::new::<SaLo>()
        .register::<Unit>()
);

Usage

bevy_salo creates schedules for serialization and deserialization. If you have access to a &mut World, you can use these extension methods. You can either use a system or implement a custom Command.

world.load_from_file::<All>("test.ron");
world.save_to_file::<All>("test.json");
world.deserialize_from::<All>(bytes);
let bytes = world.serialize_to::<All>();

Deserialize does not remove existing items. To cleanup, choose one of these functions that best suit your use case, or write your own logic.

// remove all serialized components, does not despawn entities
world.remove_serialized_components::<All>();

// despawn entities with a marker.
world.despawn_with_marker::<Marker>();

Traits

For your structs to work with bevy_salo, you need to implement one of three traits: SaveLoadCore, SaveLoadMapped and SaveLoad.

SaveLoadCore

SaveLoadCore can be easily implemented on any struct implementing serde::Serialize and serde::Deserialize.

struct Weapon {
    name: String,
    damage: f32,
    cost: i32,
}
impl SaveLoadCore for Weapon {}

However you should almost always overwrite the type_name function on SaveLoadCore, since the default implementation Any::type_name() is unstable across rust verions and namespace dependent, which could break the save format when refactoring.

impl SaveLoadCore for Weapon {
    // This has to be unique across all registered types.
    fn type_name() -> Cow<'static, str> {
        Cow::Borrowed("weapon")
    }
    // Provide a path name for the associated entity.
    fn path_name(&self) -> Option<Cow<'static, str>> {
        Some(self.name.clone().into())
    }
}

SaveLoadMapped

SaveLoadMapped is just like SaveLoadCore but you can map non-serializable struct into a serializable.

SaveLoad

Implementing SaveLoad allows you to do arbitrary things during serialization and deserialization. Checkout its documentation for more information.

String interning example:

interned_enum!(ElementsServer, Elements: u64 {
    Water, Earth, Fire, Air
});

impl SaveLoad for Elements {
    type Ser<'ser> = &'ser str;
    type De = String;
    type Context<'w, 's> = Res<'w, ElementsServer>;
    type ContextMut<'w, 's> = ResMut<'s, ElementsServer>;

    fn to_serializable<'t, 'w, 's>(&'t self, 
        _: Entity,
        _: impl Fn(Entity) -> EntityPath, 
        res: &'t Res<'w, ElementsServer>
    ) -> Self::Ser<'t> {
        res.as_str(*self)
    }

    fn from_deserialize<'w, 's>(
        de: Self::De, 
        _: &mut Commands,
        _: Entity,
        _: impl FnMut(&mut Commands, &EntityPath) -> Entity, 
        res: &mut ResMut<'s, ElementsServer>
    ) -> Self {
        res.get(&de)
    }
}

Paths

bevy_salo records each entity as either its Entity ID or its path. Entity ID is only used for disambiguation, while path allow matching with existing entity.

Each component can optionally provide a name with the path_name function defined in the aforementioned traits for their associated entity. The PathName component can be used instead for non-serialized entities.

In this example the entity has the path name "John".

Entity {
    Character => Some("John"),
    Weapon => None,
    Armor => None,
}

This panics for conflicting names.

Entity {
    Character => Some("John"),
    Role => Some("Protagonist"),
}

An entity's path contains all its named ancestors. Consider this entity:

(root)::window::(unnamed)::characters::John::weapon

The weapon's path is characters::John::weapon, while everything before its unnamed ancestor is ignored. This is helpful when you want to insert "John" into an existing entity "characters".

Pathed entities must have unique paths, but duplicated names are allowed.

// legal, although both named `weapon`, paths are different
characters::John::weapon
characters::Jane::weapon

// illegal, 2 entities with path `characters`
characters::John::weapon
characters::Jane::(unnamed)::characters

Warnings

When serializing, non-serializing parents of serialized children must be named.

// legal, parent is root
(root)::[Named]

// legal, parent is named
Named::[Named]

// illegal, parent is not named, cannot deserialize correctly
(unnamed)::[Named]

PathName is not serialized and should not be used in non-static serialized entities.

Dependencies

~23MB
~448K SLoC