bevy_mod_scripting icon indicating copy to clipboard operation
bevy_mod_scripting copied to clipboard

Implement MapEntity for ScriptValue to facilitate saving

Open Ownezx opened this issue 4 months ago • 3 comments

As I have started to combine saving with BMS, I realize that what seems to be used in Bevy to update Entity IDs throughout state change is the MapEntities and ReflectMapEntities traits.

Using these traits, bevy_save can very easily remap links to entities within components and allows the user to avoid having to update references manually, using the bevy EntityMapper instead.

Ideally, I would like ScriptValue to implement the MapEntity trait in order to allow users to update references to entities within script parameters. This is already possible for components as we have access to ReflectBase::Component(Entity, ComponentId) Adding a enum type ReflectBase::Entity(Entity) could allow to easily implement the MapEntity trait.

One challenge to consider is the behavior when a reflect of something else than a component or entity is mapped. I would assume the reference doesn't always stays valid? If not we probably should panic to avoid undefined behavior, while allowing bypassing this by adding a no_map_panic feature?


As a proof of concept I implemented MapEntities successfully for components and used it with bevy_save to apply snapshot with the following logic:

  • Saving
    • player presses F5
    • on_save lua call script sends ScriptValue I save it in a resources
    • bevy_save does a snap shot that includes this resource.
  • Loading
    • player presses F9
    • bevy_save applies snapshot (this is where ReflectMapEntities is applied to remap to the new entities )
    • I send the data back to script with an on_load lua call.

Here are a few snippets for the MapEntity and Luau script used

#[derive(Default, Reflect, Debug, Clone)]
pub struct LocalScriptValue(pub ScriptValue);

impl MapEntities for LocalScriptValue {
    fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
        self.0 = match &self.0 {
            ScriptValue::Unit => ScriptValue::Unit,
            ScriptValue::Bool(val) => ScriptValue::Bool(*val),
            ScriptValue::Integer(val) => ScriptValue::Integer(*val),
            ScriptValue::Float(val) => ScriptValue::Float(*val),
            ScriptValue::String(cow) => ScriptValue::String(cow.clone()),
            ScriptValue::List(script_values) => {
                let mut vec = Vec::with_capacity(script_values.len());
                for val in script_values {
                    let mut temp = LocalScriptValue(val.clone());
                    temp.map_entities(entity_mapper);
                    vec.push(temp.0);
                }
                ScriptValue::List(vec)
            }
            ScriptValue::Map(hash_map) => {
                let mut map: HashMap<String, ScriptValue> = HashMap::with_capacity(hash_map.len());
                for (key, value) in hash_map.iter() {
                    let mut temp = LocalScriptValue(value.clone());
                    temp.map_entities(entity_mapper);
                    map.insert(key.to_string(), temp.0);
                }
                ScriptValue::Map(map)
            }
            ScriptValue::Reference(reflect_reference) => {
                match &reflect_reference.base.base_id {
                    ReflectBase::Component(entity, component_id) => {
                        let mapped = entity_mapper.map_entity(*entity);

                        let mut new_ref = reflect_reference.clone();
                        new_ref.base.base_id = ReflectBase::Component(mapped, *component_id);

                        ScriptValue::Reference(new_ref)
                    }
                    // resources and owned allocations don’t need entity remapping
                    _ => {
                        error!("Attempted to map reflect_reference, that was not component");
                        ScriptValue::Unit
                    }
                }
            }
            ScriptValue::FunctionMut(_) => {
                error!("Attempted to map dynamic_script_function_mut");
                ScriptValue::Unit
            }
            ScriptValue::Function(_) => {
                error!("Attempted to map dynamic_script_function");
                ScriptValue::Unit
            }
            ScriptValue::Error(interop_error) => ScriptValue::Error(interop_error.clone()),
        };
    }
}
local Template = require("./../library/spawnTemplate")
local simple = require("./../templates/FirstTemplates")

local scenario = "missile_test"

register_scenario(
    scenario,
    "Test",
    "Test scenario",
    {}
)

type SavedData = {
    ship: any,
    ended: boolean,
    vel: any,
} 
local s: SavedData;

function start_scenario(data)
    s =
    {
        missile = nil;
        ship = nil;
        ended = false;
    }
    s.ship = Template.spawnTemplate(simple.corvette, 0, 0);
    local velocity = construct(types.LinearVelocity, {})
    velocity["1"].x = 50
    velocity["1"].y = -50
    world.insert_component(s.ship, types.LinearVelocity, velocity)
    s.vel = world.get_component(s.ship, types.LinearVelocity)
end

function fixed_update(delta: number)
    if not s.ship and not s.ended then
        print("Ship dead")
    end
    if not s.ship then
        s.ended = true;
    end

    print(s.vel["1"].x)
end

function on_script_unloaded()
    return s
end

function on_script_reloaded(saved_data: SavedData)
    s = saved_data;
end

function save_scenario()
    print("save call")
    on_save_callback(s);
end

function load_scenario(saved_data: SavedData)
    print("load call")
    s = saved_data;
end

Ownezx avatar Aug 20 '25 22:08 Ownezx

After more discussion and work here are the avenues I have explored.

I was recommended to work on the possibility of ScriptValue::Entity instead of ReflectBase. Unfortunately that can be difficult because of what we want to put in the enum value:

  • ScriptValue::Entity(Entity) makes it difficult to go to and from script bindings
  • ScriptValue::Entity(ReflectReference) makes it difficult implement MapEntities for ScriptValue
  • ScriptValue::Entity(Entity, ReflectReference) males it difficult to implement things like from::<Entity>

Other aproach to saving that were proposed was to create a resource that could be used to recreate a save state, this would allow for entities to be remapped, but would force the scripter to adhere to a pre defined save format at compile time.

Ultimately I did not find any solution that felt like was the right approach.

The workaround I have found for the moment is to have a GameEntity(Entity) component refering to it's own entity which implements the MapEntity trait, and use the above code to implement MapEntity for ScriptEntity. Although inelegant, This allows updated reference to the entity object when reloading the script, while leaving the save struct completely at the liberty of the scripter.

Ownezx avatar Aug 22 '25 16:08 Ownezx

Handling #12 would likely make this issue much easier to tackle.

Ownezx avatar Aug 25 '25 03:08 Ownezx

An example workaround example is shown on #12

Ownezx avatar Aug 27 '25 01:08 Ownezx