Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 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

ConceptOld SystemNew SystemNotes / Implications
Property Type StructureTwo 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 PropertiesDefine 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 for a particular Entity E.
Entity AssociationImplicit—only one entity ("person")Every property must explicitly specify the Entity it belongs to (e.g., Person); Entities are defined separately.
Default ValuesProvided in the macro creating the property-identifier type.Same but with updated syntax; default values are per Property implementation.
Using Existing TypesA single value type can be used in multiple properties—including primitive types like u8Only 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 BehaviorMacros 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

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. In our case, it defines the type alias PersonId = EntityId.

Adding a new entity (e.g. a new person)

Adding a new entity with multiple property values:

// Assuming the `Person` entity is defined somewhere.
// Add a new entity (a person in this case) to an existing `Context` instance we have access to.
let person_id = context.add_entity((Age(25), InfectionStatus::Infected)).unwrap();

Observe:

  • The compiler is smart enough to know that we are adding a new Person entity because we supplied a list of property values that are properties for a Person.
  • The add_entity function takes a "property list", which is just a tuple of 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.
  • A single-property tuple uses the syntax (Age(25), ). Notice the awkward trailing comma, which lets the compiler know the parentheses are defining a tuple rather than functioning as grouping an expression.

Adding a new entity with just one property value:

// Assuming the `Person` entity is defined somewhere.
// Add a new entity (a person in this case) to an existing `Context` instance we have access to.
let person_id = context.add_entity((Age(25), )).unwrap();

Adding a new entity with only default property values:

// If you specify the `EntityId<E>` return type, the compiler uses it to infer which entity to add.
// This is a good practice and avoids the special "turbo fish" syntax.
let person_id: PersonId = context.add_entity(()).unwrap();

// If we don't specify the `EntityId<E>` type, we have to explicitly tell the compiler *which* entity
// type we are adding, as there is nothing from which to infer the entity type.
let person_id = context.add_entity::<Person, _>(()).unwrap();

(These two examples assume there are no required properties, that is, that every property has a default value.)

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);
    });
}