Migration to Ixa 2.0
Properties in the new Entity system
In the new Entity system, properties work a little differently. We now have a chapter devoted to properties in the Ixa Book. This section of the migration guide describes the changes in the new system relative to the old system.
The Old Way to Define a Property
Previously a property consisted of two distinct types: the type of the property’s value, and the type that identifies the property itself.
// The property _value_ type, any regular Rust datatype that implements
// `Copy` and a few other traits, which we define like any other Rust type.
#[derive(Debug, Hash, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub enum InfectionStatusValue {
Susceptible,
Infected,
Recovered,
}
// The type identifying the property itself. A macro defines this as a ZST and
// generates the code connecting it to the property value type above.
define_person_property_with_default!(
InfectionStatus, // Property Name
InfectionStatusValue, // Type of the Property Values
InfectionStatusValue::Susceptible // Default value used when a person is added to the simulation
);
The only entity in the old system is a person, so there’s no need to specify
that this is a property for the Person entity.
The New Way to Define a Property
In the new system, we combine these two types into a single type:
// We now have an `Entity` defined somewhere.
define_entity!(Person);
// This macro takes the entire type declaration itself as the first argument.
define_property!(
enum InfectionStatus {
Susceptible,
Infected,
Recovered,
},
Person,
default_const = InfectionStatus::Susceptible
);
If you want to use an existing type for a property, or if you want to make the
same type a property of more than one Entity, you can use the impl_property!
variant:
// The downside is, we have to make sure the property type implements all the
// traits a property needs.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize)]
pub enum InfectionStatus {
Susceptible,
Infected,
Recovered,
}
// Implements `Property<Person>` for an existing type.
impl_property!(
InfectionStatus,
Person,
default_const = InfectionStatus::Susceptible
);
In the ordinary derive path, define_property! tacks on the derive traits and
pub visibility to the type definition and then calls impl_property! as
above. For property types that cannot derive PartialEq / Eq or Hash,
define_property! can also generate those implementations with
impl_eq_hash = …; manually declared types can use impl_property_eq!,
impl_property_hash!, or impl_property_eq_hash! for the same generated
behavior, or client code can implement these traits itself.
The crucial thing to understand is that the value type is the property type.
The impl Property<Person> for InfectionStatus, which the macros give you, is
the thing that ties the InfectionStatus type to the Person entity.
For details about defining properties, see the property_impl module-level docs
and API docs for the macros define_property!, impl_property!,
impl_property_eq!, impl_property_hash!, impl_property_eq_hash!,
define_derived_property!, impl_derived_property!, and define_multi_property!.
The API docs give multiple examples.
Multi-property definitions now take the entity first and the component properties as a tuple:
define_multi_property!(Person, (Age, InfectionStatus));
Replace the old tuple-first form
define_multi_property!((Age, InfectionStatus), Person) and the old flat form
define_multi_property!(Person, Age, InfectionStatus) with the new form above.
Multi-properties still require at least two component properties.
Summary
| Concept | Old System | New System | Notes / Implications |
|---|---|---|---|
| Property Type Structure | Two separate types: (1) value type, and (2) property-identifier ZST. | A single type represents both the value and identifies the property. | Simplified to a single type |
| Defining Properties | Define a normal Rust type (or use an existing primitive, e.g. u8), then use a macro to define the identifying property type. | Define a normal Rust type, then use impl_property to declare it a Property<E> for a particular Entity E. | |
| Entity Association | Implicit—only one entity (“person”) | Every property must explicitly specify the Entity it belongs to (e.g., Person); Entities are defined separately. | |
| Default Values | Provided in the macro creating the property-identifier type. | Same but with updated syntax; default values are per Property<E> implementation. | |
| Using Existing Types | A single value type can be used in multiple properties—including primitive types like u8 | Only one property per type (per Entity); primitive types must be wrapped in a newtype. | Both systems require that the existing type implement the required property traits. |
| Macro Behavior | Macros define the property’s ZST and connect it to the value type via trait impls. | Macros define “is a property of entity E” relationship via trait impl. No additional synthesized types. | Both enforce correctness via macro |
Global property validator errors
Global-property validators are client code, so they should return
Result<(), Box<dyn std::error::Error + ’static>> instead of constructing
IxaError values directly. Ixa wraps any returned validator error in
IxaError::IllegalGlobalPropertyValue when a global property is set or loaded.
New Entities API: How Do I…?
We will use Person as an example entity, but there is nothing special about
Person. We could just as easily define a School entity, or an Animal
entity.
Defining a new Entity
Use the ixa::define_entity! macro:
define_entity!(Person);
This both declares the type Person and implements the Entity trait for it.
If you want to implement the Entity trait for a type you have already
declared, use the impl_entity! macro instead:
#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
pub struct Person;
impl_entity!(Person);
These macros automatically create a type alias of the form
MyEntityId = EntityId<MyEntity>. In our case, it defines the type alias
PersonId = EntityId<Person>.
Adding a new entity (e.g. a new person)
Adding a new entity with only default property values by passing the entity type directly:
let person_id = context.add_entity(Person).unwrap();
(This example assumes there are no required properties, that is, that every property has a default value.)
Adding a new entity with property values using the with! macro:
let person_id = context.add_entity(with!(Person, Age(25), InfectionStatus::Infected)).unwrap();
The with! macro takes the entity type as its first argument, followed by any
property values. The properties must be distinct, of course, and there must be
a value for every “required” property, that is, for every (non-derived)
property that doesn’t have a default value.
Public entity initialization no longer accepts naked tuples such as
(Age(25), InfectionStatus::Infected). Use with!(Person, …) when you want
to provide one or more property values.
Adding a new entity with just one property value:
let person_id = context.add_entity(with!(Person, Age(25))).unwrap();
To initialize an entity with only default values, pass the entity type directly:
let person_id = context.add_entity(Person).unwrap();
The same pattern applies to queries. Use with!(Person, …) to filter by property values, or use Person when you
want to work with the entire population. Public query APIs no longer accept naked tuples such as (Age(25),).
Getting a property value for an entity
// The compiler knows which property to fetch because of the type of the return value we have specified.
let age: Age = context.get_property(person_id);
In the rare situation in where the compiler cannot infer which property to fetch, you can specify the property explicitly using the turbo fish syntax. We recommend you always write your code in such a way that you can use the first version.
let age = context.get_property::<Person, Age>(person_id);
Setting a property value for an entity
// The compiler is always able to infer which entity and property using the `EntityId` and type
// of the property value.
context.set_property(person_id, Age(35));
Index a property
// This method is called with the turbo-fish syntax, because there is no way for the compiler to infer
// the entity and property types.
context.index_property::<Person, Age>();
It is not an error to call this method multiple times for the same property. All calls after the first one will just be ignored.
Subscribe to events
The only difference is that now we use the
PropertyChangeEvent<E: Entity, P: Property<E>> and
EntityCreatedEvent<E: Entity> types.
pub fn init(context: &mut Context) {
context.subscribe_to_event(
move |context, event: PropertyChangeEvent<Person, InfectionStatus>| {
handle_infection_status_change(context, event);
},
);
context.subscribe_to_event(move |context, event: PropertyChangeEvent<Person, Alive>| {
handle_person_removal(context, event);
});
}