In order to properly support serialization for types that contain an Entity, we need a way to create an intermediate type that converts entities and other non-stable data into a serialization-safe form. The idea is that any type that can be directly serialized has itself as the intermediate type, but types that need preprocessing before being serialized can specify a different type to use for serialization, and can be converted to and from that intermediate representation. This expands on the ideas present in saveload, but should provide a more flexible approach that doesn't require all types to be known at compile time.
Background and Motivation
The initial motivation for this idea is that Entity is not Serialize. Not allowing entities to be serialized directly is a deliberate design decision by the specs developers, and one that I think is entirely reasonable. Entities are meant to be ephemeral, and neither specs nor Amethyst make any guarantees about consistency in the actual entity IDs generated at runtime; That is to say, the same logic object in your game world may have a different entity ID each time you run your game. For cases like networking and prefab saving/loading, it's not practical to try to serialize the entity ID directly, since the entity ID won't necessarily be the same between client and server (in the case of networking).
For the purposes of a read-only editor, it's actually fine for us to serialize the entity directly. Since the editor only needs to know the entity values in order to display components by entity, and it will never try to reuse an entity ID across sessions, there's nothing that can break as a result of using entity IDs directly. In fact, for an editor, it's desirable to be able to see the actual entity IDs being used within an active session of a game for debugging purposes.
However, even with a read-only editor we quickly run into limitations resulting from Entity not being Serialize. The biggest of such issues is that we can't serialize components that have an Entity as a member (and therefore we can't view such components in the editor). As a short-term workaround for this we provide the SerializableEntity type, but that solution is very specific to this crate and only serves to obfuscate the larger problem of finding stable IDs to use in place of entity IDs for serialization.
Current Solution: saveload
The currently recommended solution is is the saveload module that specs provides. saveload provides functionality specifying stable "marker" values to be used instead of entity IDs when serializing a group of components. This core concept of allowing the user (or some external system) to provide stable marker values that are specific to the current context works well, however the specific implementation for saveload has some drawbacks that make it unsuitable in my estimation:
- Only addresses
Entity values for component grouping but still doesn't support replacing Entity values that are used within a component. This means that you still can't use saveload to serialize a component that has an Entity as one of its members.
- All component types must be known at compile time because the implementation is provided over a tuple of component types, e.g. you must specify something like
(Transform, Light, MyFoo, MyBar) and only those specified component types will be serialized.
- Component data is grouped around entities rather than all components of a given type being serialized into a flat array. This setup is useful for things that are meant to be human-facing since grouping an entity's components together is easier to read, but it's an inherently inefficient solution for serializing and storing large amounts of component data.
The proposed solution builds on the approach taken by saveload, but attempts to solve some of the ergonomic and functional issues that it has.
Proposed Solution
Rather than directly using the Serialize impl on a component, we should introduce an intermediate trait SerializeIntermediate which is able to produce a serialization-safe intermediate value:
pub trait SerializeIntermediate {
type Intermediate: Serialize;
fn to(&self) -> Self::Intermediate;
fn from(from: Self::Intermediate) -> Self;
}
Any type that can't be directly serialized (e.g. because it has an Entity member) could instead specify an alternate type that can be used, and the internal serialization mechanism would first convert each instance to the intermediate representation before actually serializing the data.
This solution can work for deserialization as well: As long as the component knows how to convert from the intermediate representation back into the concrete type, then we can handle deserialization by deserializing into the intermediate representation first, then converting back to the main type.
Example: Parent Component
The Parent component is a commonly-used component that can't be serialized (and therefore can't be viewed in the editor) today. Using SerializeIntermediate, we can instead convert to a second type that uses a stable marker to describe the hierarchy:
pub struct ParentIntermediate {
pub entity: EntityMarker,
}
impl SerializeIntermediate for Parent {
type Intermediate = ParentIntermediate;
fn to(&self) -> Self::Intermediate {
let marker = ...; // Lookup marker corresponding to entity.
ParentIntermediate {
entity: marker,
}
}
fn from(from: Self::Intermediate) -> Self {
let entity = ...; // Lookup entity corresponding to the marker.
Parent { entity }
}
}
Note that this example does not demonstrate a proper mechanism for how marker values would be generated or mapped to/from entities. See the next section for discussion on how this can be handled.
Generating Intermediate Values
The question remains of how to handle generating marker values and map them to/from entity IDs. I don't think this is something that should be handled by this crate or the serialization library, since different use cases call for different approaches (i.e. prefabs use the index within the file to determine the entity, game networking may require the server to generate unique IDs and send them to the client, the editor may want to use entity IDs directly, etc.).
Instead, I think it would be enough to allow the SerializeIntermediate trait to provide a SystemData type that can be passed in as a parameter to the conversion functions. This would allow the implementation for each type to perform any case-specific conversion that it cares to do. I suspect that this approach is not going to handle all the necessary use cases, but it simple enough to get us started.
The adjusted SerializeIntermediate would be as follows:
pub trait SerializeIntermediate {
type Intermediate: Serialize;
type Data: SystemData;
fn to(&self, data: Self::Data) -> Self::Intermediate;
fn from(from: Self::Intermediate, data: Self::Data) -> Self;
}
In order to properly support serialization for types that contain an
Entity, we need a way to create an intermediate type that converts entities and other non-stable data into a serialization-safe form. The idea is that any type that can be directly serialized has itself as the intermediate type, but types that need preprocessing before being serialized can specify a different type to use for serialization, and can be converted to and from that intermediate representation. This expands on the ideas present insaveload, but should provide a more flexible approach that doesn't require all types to be known at compile time.Background and Motivation
The initial motivation for this idea is that
Entityis notSerialize. Not allowing entities to be serialized directly is a deliberate design decision by the specs developers, and one that I think is entirely reasonable. Entities are meant to be ephemeral, and neither specs nor Amethyst make any guarantees about consistency in the actual entity IDs generated at runtime; That is to say, the same logic object in your game world may have a different entity ID each time you run your game. For cases like networking and prefab saving/loading, it's not practical to try to serialize the entity ID directly, since the entity ID won't necessarily be the same between client and server (in the case of networking).For the purposes of a read-only editor, it's actually fine for us to serialize the entity directly. Since the editor only needs to know the entity values in order to display components by entity, and it will never try to reuse an entity ID across sessions, there's nothing that can break as a result of using entity IDs directly. In fact, for an editor, it's desirable to be able to see the actual entity IDs being used within an active session of a game for debugging purposes.
However, even with a read-only editor we quickly run into limitations resulting from
Entitynot beingSerialize. The biggest of such issues is that we can't serialize components that have anEntityas a member (and therefore we can't view such components in the editor). As a short-term workaround for this we provide theSerializableEntitytype, but that solution is very specific to this crate and only serves to obfuscate the larger problem of finding stable IDs to use in place of entity IDs for serialization.Current Solution:
saveloadThe currently recommended solution is is the
saveloadmodule that specs provides.saveloadprovides functionality specifying stable "marker" values to be used instead of entity IDs when serializing a group of components. This core concept of allowing the user (or some external system) to provide stable marker values that are specific to the current context works well, however the specific implementation forsaveloadhas some drawbacks that make it unsuitable in my estimation:Entityvalues for component grouping but still doesn't support replacingEntityvalues that are used within a component. This means that you still can't usesaveloadto serialize a component that has anEntityas one of its members.(Transform, Light, MyFoo, MyBar)and only those specified component types will be serialized.The proposed solution builds on the approach taken by
saveload, but attempts to solve some of the ergonomic and functional issues that it has.Proposed Solution
Rather than directly using the
Serializeimpl on a component, we should introduce an intermediate traitSerializeIntermediatewhich is able to produce a serialization-safe intermediate value:Any type that can't be directly serialized (e.g. because it has an
Entitymember) could instead specify an alternate type that can be used, and the internal serialization mechanism would first convert each instance to the intermediate representation before actually serializing the data.This solution can work for deserialization as well: As long as the component knows how to convert from the intermediate representation back into the concrete type, then we can handle deserialization by deserializing into the intermediate representation first, then converting back to the main type.
Example:
ParentComponentThe
Parentcomponent is a commonly-used component that can't be serialized (and therefore can't be viewed in the editor) today. UsingSerializeIntermediate, we can instead convert to a second type that uses a stable marker to describe the hierarchy:Note that this example does not demonstrate a proper mechanism for how marker values would be generated or mapped to/from entities. See the next section for discussion on how this can be handled.
Generating Intermediate Values
The question remains of how to handle generating marker values and map them to/from entity IDs. I don't think this is something that should be handled by this crate or the serialization library, since different use cases call for different approaches (i.e. prefabs use the index within the file to determine the entity, game networking may require the server to generate unique IDs and send them to the client, the editor may want to use entity IDs directly, etc.).
Instead, I think it would be enough to allow the
SerializeIntermediatetrait to provide aSystemDatatype that can be passed in as a parameter to the conversion functions. This would allow the implementation for each type to perform any case-specific conversion that it cares to do. I suspect that this approach is not going to handle all the necessary use cases, but it simple enough to get us started.The adjusted
SerializeIntermediatewould be as follows: