Migration to ixa 2.0
Properties in the new Entity system
In the new Entity system, properties work a little differently.
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, Serialize)]
pub enum InfectionStatus {
Susceptible,
Infected,
Recovered,
}
// Implements `Property\<Person>` for an existing type.
impl_property!(
InfectionStatus,
Person,
default_const = InfectionStatus::Susceptible
);
(In fact, the only thing define_property! does is tack on the derive
traits and pub visibility to the type definition and then call
impl_property! as above.)
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!,
define_derived_property!, impl_derived_property!, and
define_multi_property!. The API docs give multiple examples.
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.
Adding a new entity with just one property value:
let person_id = context.add_entity(with!(Person, Age(25))).unwrap();
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 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);
});
}