Skip to content

Commit

Permalink
(De) serialize resources in scenes (bevyengine#6846)
Browse files Browse the repository at this point in the history
# Objective

Co-Authored-By: davier
[[email protected]](mailto:[email protected])
Fixes bevyengine#3576.
Adds a `resources` field in scene serialization data to allow
de/serializing resources that have reflection enabled.

## Solution

Most of this code is taken from a previous closed PR:
bevyengine#3580. Most of the credit goes to
@Davier , what I did was mostly getting it to work on the latest main
branch of Bevy, along with adding a few asserts in the currently
existing tests to be sure everything is working properly.

This PR changes the scene format to include resources in this way:
```
(
  resources: {
    // List of resources here, keyed by resource type name.
  },
  entities: [
    // Previous scene format here
  ],
)
```

An example taken from the tests:
```
(
  resources: {
    "bevy_scene::serde::tests::MyResource": (
      foo: 123,
    ),
  },
  entities: {
    // Previous scene format here
  },
)
```
For this, a `resources` fields has been added on the `DynamicScene` and
the `DynamicSceneBuilder` structs. The latter now also has a method
named `extract_resources` to properly extract the existing resources
registered in the local type registry, in a similar way to
`extract_entities`.


---

## Changelog


Added: Reflect resources registered in the type registry used by dynamic
scenes will now be properly de/serialized in scene data.

## Migration Guide

Since the scene format has been changed, the user may not be able to use
scenes saved prior to this PR due to the `resources` scene field being
missing. ~~To preserve backwards compatibility, I will try to make the
`resources` fully optional so that old scenes can be loaded without
issue.~~

## TODOs

- [x] I may have to update a few doc blocks still referring to dynamic
scenes as mere container of entities, since they now include resources
as well.
- [x] ~~I want to make the `resources` key optional, as specified in the
Migration Guide, so that old scenes will be compatible with this
change.~~ Since this would only be trivial for ron format, I think it
might be better to consider it in a separate PR/discussion to figure out
if it could be done for binary serialization too.
- [x] I suppose it might be a good idea to add a resources in the scene
example so that users will quickly notice they can serialize resources
just like entities.

---------

Co-authored-by: Carter Anderson <[email protected]>
  • Loading branch information
Carbonhell and cart authored Mar 20, 2023
1 parent 6a85eb3 commit 7b38de0
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 64 deletions.
5 changes: 5 additions & 0 deletions assets/scenes/load_scene_example.scn.ron
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
(
resources: {
"scene::ResourceA": (
score: 2,
),
},
entities: {
0: (
components: {
Expand Down
30 changes: 26 additions & 4 deletions crates/bevy_scene/src/dynamic_scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ use bevy_reflect::{Reflect, TypeRegistryArc, TypeUuid};

#[cfg(feature = "serialize")]
use crate::serde::SceneSerializer;
use bevy_ecs::reflect::ReflectResource;
#[cfg(feature = "serialize")]
use serde::Serialize;

/// A collection of serializable dynamic entities, each with its own run-time defined set of components.
/// A collection of serializable resources and dynamic entities.
///
/// Each dynamic entity in the collection contains its own run-time defined set of components.
/// To spawn a dynamic scene, you can use either:
/// * [`SceneSpawner::spawn_dynamic`](crate::SceneSpawner::spawn_dynamic)
/// * adding the [`DynamicSceneBundle`](crate::DynamicSceneBundle) to an entity
Expand All @@ -23,6 +26,7 @@ use serde::Serialize;
#[derive(Default, TypeUuid)]
#[uuid = "749479b1-fb8c-4ff8-a775-623aa76014f5"]
pub struct DynamicScene {
pub resources: Vec<Box<dyn Reflect>>,
pub entities: Vec<DynamicEntity>,
}

Expand All @@ -47,15 +51,16 @@ impl DynamicScene {
DynamicSceneBuilder::from_world_with_type_registry(world, type_registry.clone());

builder.extract_entities(world.iter_entities().map(|entity| entity.id()));
builder.extract_resources();

builder.build()
}

/// Write the dynamic entities and their corresponding components to the given world.
/// Write the resources, the dynamic entities, and their corresponding components to the given world.
///
/// This method will return a [`SceneSpawnError`] if a type either is not registered
/// in the provided [`AppTypeRegistry`] resource, or doesn't reflect the
/// [`Component`](bevy_ecs::component::Component) trait.
/// [`Component`](bevy_ecs::component::Component) or [`Resource`](bevy_ecs::prelude::Resource) trait.
pub fn write_to_world_with(
&self,
world: &mut World,
Expand All @@ -64,6 +69,23 @@ impl DynamicScene {
) -> Result<(), SceneSpawnError> {
let type_registry = type_registry.read();

for resource in &self.resources {
let registration = type_registry
.get_with_name(resource.type_name())
.ok_or_else(|| SceneSpawnError::UnregisteredType {
type_name: resource.type_name().to_string(),
})?;
let reflect_resource = registration.data::<ReflectResource>().ok_or_else(|| {
SceneSpawnError::UnregisteredResource {
type_name: resource.type_name().to_string(),
}
})?;

// If the world already contains an instance of the given resource
// just apply the (possibly) new value, otherwise insert the resource
reflect_resource.apply_or_insert(world, &**resource);
}

for scene_entity in &self.entities {
// Fetch the entity with the given entity id from the `entity_map`
// or spawn a new entity with a transiently unique id if there is
Expand Down Expand Up @@ -105,7 +127,7 @@ impl DynamicScene {
Ok(())
}

/// Write the dynamic entities and their corresponding components to the given world.
/// Write the resources, the dynamic entities, and their corresponding components to the given world.
///
/// This method will return a [`SceneSpawnError`] if a type either is not registered
/// in the world's [`AppTypeRegistry`] resource, or doesn't reflect the
Expand Down
129 changes: 115 additions & 14 deletions crates/bevy_scene/src/dynamic_scene_builder.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use crate::{DynamicEntity, DynamicScene};
use bevy_app::AppTypeRegistry;
use bevy_ecs::{prelude::Entity, reflect::ReflectComponent, world::World};
use bevy_ecs::component::ComponentId;
use bevy_ecs::{
prelude::Entity,
reflect::{ReflectComponent, ReflectResource},
world::World,
};
use bevy_reflect::Reflect;
use bevy_utils::default;
use std::collections::BTreeMap;

/// A [`DynamicScene`] builder, used to build a scene from a [`World`] by extracting some entities.
/// A [`DynamicScene`] builder, used to build a scene from a [`World`] by extracting some entities and resources.
///
/// # Entity Order
///
Expand All @@ -31,6 +37,7 @@ use std::collections::BTreeMap;
/// let dynamic_scene = builder.build();
/// ```
pub struct DynamicSceneBuilder<'w> {
extracted_resources: BTreeMap<ComponentId, Box<dyn Reflect>>,
extracted_scene: BTreeMap<u32, DynamicEntity>,
type_registry: AppTypeRegistry,
original_world: &'w World,
Expand All @@ -41,6 +48,7 @@ impl<'w> DynamicSceneBuilder<'w> {
/// All components registered in that world's [`AppTypeRegistry`] resource will be extracted.
pub fn from_world(world: &'w World) -> Self {
Self {
extracted_resources: default(),
extracted_scene: default(),
type_registry: world.resource::<AppTypeRegistry>().clone(),
original_world: world,
Expand All @@ -51,6 +59,7 @@ impl<'w> DynamicSceneBuilder<'w> {
/// Only components registered in the given [`AppTypeRegistry`] will be extracted.
pub fn from_world_with_type_registry(world: &'w World, type_registry: AppTypeRegistry) -> Self {
Self {
extracted_resources: default(),
extracted_scene: default(),
type_registry,
original_world: world,
Expand All @@ -63,6 +72,7 @@ impl<'w> DynamicSceneBuilder<'w> {
/// [`Self::remove_empty_entities`] before building the scene.
pub fn build(self) -> DynamicScene {
DynamicScene {
resources: self.extracted_resources.into_values().collect(),
entities: self.extracted_scene.into_values().collect(),
}
}
Expand Down Expand Up @@ -126,31 +136,80 @@ impl<'w> DynamicSceneBuilder<'w> {

let entity = self.original_world.entity(entity);
for component_id in entity.archetype().components() {
let reflect_component = self
.original_world
.components()
.get_info(component_id)
.and_then(|info| type_registry.get(info.type_id().unwrap()))
.and_then(|registration| registration.data::<ReflectComponent>())
.and_then(|reflect_component| reflect_component.reflect(entity));

if let Some(reflect_component) = reflect_component {
entry.components.push(reflect_component.clone_value());
}
let mut extract_and_push = || {
let type_id = self
.original_world
.components()
.get_info(component_id)?
.type_id()?;
let component = type_registry
.get(type_id)?
.data::<ReflectComponent>()?
.reflect(entity)?;
entry.components.push(component.clone_value());
Some(())
};
extract_and_push();
}
self.extracted_scene.insert(index, entry);
}

drop(type_registry);
self
}

/// Extract resources from the builder's [`World`].
///
/// Only resources registered in the builder's [`AppTypeRegistry`] will be extracted.
/// Re-extracting a resource that was already extracted will have no effect.
/// ```
/// # use bevy_scene::DynamicSceneBuilder;
/// # use bevy_app::AppTypeRegistry;
/// # use bevy_ecs::prelude::{ReflectResource, Resource, World};
/// # use bevy_reflect::Reflect;
/// #[derive(Resource, Default, Reflect)]
/// #[reflect(Resource)]
/// struct MyResource;
///
/// # let mut world = World::default();
/// # world.init_resource::<AppTypeRegistry>();
/// world.insert_resource(MyResource);
///
/// let mut builder = DynamicSceneBuilder::from_world(&world);
/// builder.extract_resources();
/// let scene = builder.build();
/// ```
pub fn extract_resources(&mut self) -> &mut Self {
let type_registry = self.type_registry.read();
for (component_id, _) in self.original_world.storages().resources.iter() {
let mut extract_and_push = || {
let type_id = self
.original_world
.components()
.get_info(component_id)?
.type_id()?;
let resource = type_registry
.get(type_id)?
.data::<ReflectResource>()?
.reflect(self.original_world)?;
self.extracted_resources
.insert(component_id, resource.clone_value());
Some(())
};
extract_and_push();
}

drop(type_registry);
self
}
}

#[cfg(test)]
mod tests {
use bevy_app::AppTypeRegistry;
use bevy_ecs::{
component::Component, prelude::Entity, query::With, reflect::ReflectComponent, world::World,
component::Component, prelude::Entity, prelude::Resource, query::With,
reflect::ReflectComponent, reflect::ReflectResource, world::World,
};

use bevy_reflect::Reflect;
Expand All @@ -160,10 +219,15 @@ mod tests {
#[derive(Component, Reflect, Default, Eq, PartialEq, Debug)]
#[reflect(Component)]
struct ComponentA;

#[derive(Component, Reflect, Default, Eq, PartialEq, Debug)]
#[reflect(Component)]
struct ComponentB;

#[derive(Resource, Reflect, Default, Eq, PartialEq, Debug)]
#[reflect(Resource)]
struct ResourceA;

#[test]
fn extract_one_entity() {
let mut world = World::default();
Expand Down Expand Up @@ -303,4 +367,41 @@ mod tests {
assert_eq!(scene.entities.len(), 1);
assert_eq!(scene.entities[0].entity, entity_a.index());
}

#[test]
fn extract_one_resource() {
let mut world = World::default();

let atr = AppTypeRegistry::default();
atr.write().register::<ResourceA>();
world.insert_resource(atr);

world.insert_resource(ResourceA);

let mut builder = DynamicSceneBuilder::from_world(&world);
builder.extract_resources();
let scene = builder.build();

assert_eq!(scene.resources.len(), 1);
assert!(scene.resources[0].represents::<ResourceA>());
}

#[test]
fn extract_one_resource_twice() {
let mut world = World::default();

let atr = AppTypeRegistry::default();
atr.write().register::<ResourceA>();
world.insert_resource(atr);

world.insert_resource(ResourceA);

let mut builder = DynamicSceneBuilder::from_world(&world);
builder.extract_resources();
builder.extract_resources();
let scene = builder.build();

assert_eq!(scene.resources.len(), 1);
assert!(scene.resources[0].represents::<ResourceA>());
}
}
29 changes: 28 additions & 1 deletion crates/bevy_scene/src/scene.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use bevy_app::AppTypeRegistry;
use bevy_ecs::{
entity::EntityMap,
reflect::{ReflectComponent, ReflectMapEntities},
reflect::{ReflectComponent, ReflectMapEntities, ReflectResource},
world::World,
};
use bevy_reflect::TypeUuid;
Expand Down Expand Up @@ -61,6 +61,33 @@ impl Scene {
};

let type_registry = type_registry.read();

// Resources archetype
for (component_id, _) in self.world.storages().resources.iter() {
let component_info = self
.world
.components()
.get_info(component_id)
.expect("component_ids in archetypes should have ComponentInfo");

let type_id = component_info
.type_id()
.expect("reflected resources must have a type_id");

let registration =
type_registry
.get(type_id)
.ok_or_else(|| SceneSpawnError::UnregisteredType {
type_name: component_info.name().to_string(),
})?;
let reflect_resource = registration.data::<ReflectResource>().ok_or_else(|| {
SceneSpawnError::UnregisteredResource {
type_name: component_info.name().to_string(),
}
})?;
reflect_resource.copy(&self.world, world);
}

for archetype in self.world.archetypes().iter() {
for scene_entity in archetype.entities() {
let entity = *instance_info
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_scene/src/scene_spawner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub struct SceneSpawner {
pub enum SceneSpawnError {
#[error("scene contains the unregistered component `{type_name}`. consider adding `#[reflect(Component)]` to your type")]
UnregisteredComponent { type_name: String },
#[error("scene contains the unregistered resource `{type_name}`. consider adding `#[reflect(Resource)]` to your type")]
UnregisteredResource { type_name: String },
#[error("scene contains the unregistered type `{type_name}`. consider registering the type using `app.register_type::<T>()`")]
UnregisteredType { type_name: String },
#[error("scene does not exist")]
Expand Down
Loading

0 comments on commit 7b38de0

Please sign in to comment.