Attribute Macro dynec::system

source ·
#[system]
Expand description

Converts a function into a system.

This macro converts the function into a unit struct with the same name that implements system::Spec. The unit struct also derefs to a function pointer, so it is still possible to call the function directly (mainly useful in unit tests) without any change in the signature. However it is not recommended to call this function directly in production code.

§Options

Options are applied behind the attribute name in the form #[system(...)]. Multiple options are separated by commas.

§name = $expr

Sets the name of the system to $expr. By default, the name is concat!(module_path!(), "::", $function_identifier).

The $expr can read the local and param states of the system directly. Since the debug name is only used for display purposes, it is allowed (although confusing to the programmer) to use mutable states in the name. It is unspecified whether debug messages use the initial debug name or the updated state.

§before($expr1, $expr2, ...) and after($expr1, $expr2, ...)

Indicates that the system must be executed before/after all partitions given in the expressions.

Similar to name, the expressions can read local and param states directly. However, only the expressions are only resolved once before the first run of the system, so mutating states has no effect on the system schedule.

§Parameters

Each parameter of a system function has a special meaning:

§Local states

Parameters with the attribute #[dynec(local(initial = xxx))] are “local states”, where xxx is an expression that evaluates to the initial value of the state.

Local states must take the type &T or &mut T, where T is the actual stored state. The mutated state persists for each instance of the system.

Use global states instead if the local state needs to be accessed from multiple systems.

Since entity references can be stored in local states, the struct used to store local states also implements entity::Referrer. The corresponding entity and not_entity attributes can be inside the local() instead.

Unlike global states, local states do not need to specify thread safety. Thread safety of local states is checked at compile time when the system is passed to the scheduler.

§Syntax reference

#[dynec(local(
    // Required, the initial value of the local state.
    initial = $expr,
    // Optional, equivalent to #[entity] in #[derive(EntityRef)].
    entity,
    // Optional, equivalent to #[not_entity] in #[derive(EntityRef)].
    not_entity,
))]

§Param states

Parameters with the attribute #[dynec(param)] are “param states”. The user has to pass initial values for param states in the .build() method. Param states behave identically to local states except for different definition location of the initial value.

It is typically used to initialize systems with resources that cannot be created statically (e.g. system canvas resources), or to schedule multiple systems declared from the same function (e.g. working on multiple discriminants of an isotope component).

Similar to local states, param states can also use entity and not_entity.

§Syntax reference

#[dynec(param(
    // Optional, equivalent to #[entity] in #[derive(EntityRef)].
    entity,
    // Optional, equivalent to #[not_entity] in #[derive(EntityRef)].
    not_entity,
))]

§Global states

Parameters with the attribute #[dynec(global)] are “global states”. Global states are shared scalar data between multiple systems. See Global for more information.

Thread-unsafe (non-Send + Sync) global states must be declared as #[dynec(global(thread_local))] to indicate that the global state can only be accessed from the main thread. As a result, systems that request thread-local global states will only be scheduled on the main thread.

§Syntax reference

#[dynec(global(
    // Optional, indicates that the global state is not thread-safe.
    // Forgetting to mark `thread_local` will result in compile error.
    thread_local,
    // Optional, acknowledges that the entities of the specified archetypes
    // contained in the global state may be uninitialized.
    maybe_uninit($ty, $ty, ...),
))]

§Simple components

Parameters of type ReadSimple<A, C> or WriteSimple<A, C>, request access to a simple component of type C from entities of the archetype A. The latter provides mutable and exclusive access to the component storages.

§Using other aliases

Using type aliases/renamed imports for the types is also allowed, but the macro would be unable to infer type parameters and mutability. In such cases, they must be indicated explicitly in the attribute. See the syntax reference below for details.

§Uninitialized entity references

If C contains references to entities of some archetype T, the scheduler automatically enforces that the system runs before any systems that create entities of archetype T, because components for entities created through EntityCreator are uninitialized until the current cycle completes. Use the maybe_uninit attribute to remove this ordering limitation.

See EntityCreationPartition for more information.

§Syntax reference

#[dynec(simple(
    // Optional, specifies the archetype and component explicitly.
    // Only required when the parameter type is not `ReadSimple`/`WriteSimple`.
    arch = $ty, comp = $ty,
    // Optional, indicates that the component access is exclusive explicitly.
    // Only required when the parameter type is not `WriteSimple`.
    mut,
    // Optional, acknowledges that the entities of the specified archetypes
    // contained in the simple components may be uninitialized.
    maybe_uninit($ty, $ty, ...),
))]

§Isotope components

Parameters of type (Read|Write)Isotope(Full|Partial) request access to an isotope component of type C from entities of the archetype A. The Write variants provide mutable and exclusive access to the component storages.

§Partial isotope access

If ReadIsotopePartial or WriteIsotopePartial is used, the system only requests access to specific discriminants of the isotope component. The actual discriminants are specified with an attribute:

#[dynec(isotope(discrim = discrim_set))] param_name: impl ReadIsotope<A, C, K>,

The expression discrim_set contains the set of discriminants requested by this system contained in an implementation of discrim::Set<C::Discrim>, which is typically an array or a Vec. The expression may reference param states directly. The expression is only evaluated once before the first run of the system, so it will not react to subsequent changes to the param states.

K is the type of the key to index the discriminant set.

See the documentation of discrim::Set for more information.

§Using other aliases

Using type aliases/renamed imports for the types is also allowed, but the macro would be unable to infer type parameters and mutability. In such cases, they must be indicated explicitly in the attribute. See the syntax reference below for details.

§Uninitialized entity references

If C contains references to entities of some archetype T, the scheduler automatically enforces that the system runs before any systems that create entities of archetype T, because components for entities created through EntityCreator are uninitialized until the current cycle completes. Use the maybe_uninit attribute to remove this ordering limitation.

See EntityCreationPartition for more information.

§Syntax reference

#[dynec(isotope(
    // Required if and only if the type is ReadIsotopePartial or WriteIsotopePartial.
    discrim = $expr,
    // Optional, must be the same as the type of the `discrim` expression.
    // Only required when the parameter type is not `ReadIsotopePartial`/`WriteIsotopePartial`.
    // Note that `ReadIsotopePartial`/`WriteIsotopePartial` have an optional third type parameter
    // that expects the same type as `discrim_set`,
    // which is `Vec<C::Discrim>` by default.
    discrim_set = $ty,
    // Optional, specifies the archetype and component explicitly.
    // Only required when the parameter type is not `(Read|Write)Isotope(Full|Partial)`.
    arch = $ty, comp = $ty,
    // Optional, indicates that the component access is exclusive explicitly.
    // Only required when the parameter type is not `impl WriteSimple`.
    mut,
    // Optional, acknowledges that the entities of the specified archetypes
    // contained in the simple components may be uninitialized.
    maybe_uninit($ty, $ty, ...),
))]

§Entity creation

Parameters that require an EntityCreator can be used to create entities. The archetype of created entities is specified in the type bounds. Note that entity creation is asynchronous to ensure synchronization, i.e. components of the created entity are deferred until the current cycle completes.

Systems that create entities of an archetype A should be scheduled to execute after all systems that may read entity references of archetype A (through strong or weak references stored in local states, global states, simple components or isotope components). See EntityCreationPartition for more information.

If it can be ensured that the new uninitialized entities cannot be leaked to other systems, e.g. if the created entity ID is not stored into any states, the attribute #[dynec(entity_creator(no_partition))] can be applied on the entity-creating parameter to avoid registering the automatic dependency to run after EntityCreationPartition<A>.

§Syntax reference

/// This attribute is not required unless `EntityCreator` is aliased.
#[dynec(entity_creator(
    // Optional, specifies the archetype if `EntityCreator` is aliased.
    arch = $ty,
    // Optional, allows the derived system to execute before
    // the EntityCreationPartition of this archetype.
    no_partition,
))]

§Entity deletion

Parameters that require an EntityDeleter can be used to delete entities. The archetype of deleted entities is specified in the type bounds. Note that EntityDeleter can only be used to mark entities as “deleting”; the entity is only deleted after all finalizer components are unset.

It is advisable to execute finalizer-removing systems after systems that mark entities for deletion finish executing. This allows deletion to happen in the same cycle, thus slightly reducing entity deletion latency (but this is not supposed to be critical anyway). Nevertheless, unlike entity creation, the scheduler does not automatically enforce ordering between finalizer-manipulating systems and entity-deleting systems.

§Syntax reference

/// This attribute is not required unless `EntityDeleter` is aliased.
#[dynec(entity_deleter(
    // Optional, specifies the archetype if `EntityDeleter` is aliased.
    arch = $ty,
))]

§Entity iterator

Parameters that require an EntityIterator can be used to iterate over entities and zip multiple component iterators. See the documentation for EntityIterator for details.

§Syntax reference

/// This attribute is not required unless `EntityIterator` is aliased.
#[dynec(entity_iterator(
    // Optional, specifies the archetype if `EntityIterator` is aliased.
    arch = $ty,
))]

§Example

use dynec::system;

#[dynec::global(initial = Title("hello world"))]
struct Title(&'static str);

#[derive(Debug, PartialEq, Eq, Hash)]
struct Foo;

dynec::archetype!(Player);

#[dynec::comp(of = Player)]
struct PositionX(f32);
#[dynec::comp(of = Player)]
struct PositionY(f32);

#[dynec::comp(of = Player)]
struct Direction(f32, f32);

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dynec::Discrim)]
struct SkillType(usize);

#[dynec::comp(of = Player, isotope = SkillType)]
struct SkillLevel(u8);

#[system(
    name = format!("simulate[counter = {}, skill_id = {:?}]", counter, skill_id),
    before(Foo),
)]
fn simulate(
    #[dynec(local(initial = 0))] counter: &mut u16,
    #[dynec(param)] &skill_id: &SkillType,
    #[dynec(global)] title: &mut Title,
    x: system::WriteSimple<Player, PositionX>,
    y: system::WriteSimple<Player, PositionY>,
    dir: system::ReadSimple<Player, Direction>,
    #[dynec(isotope(discrim = [skill_id]))] skill: system::ReadIsotopePartial<
        Player,
        SkillLevel,
        [SkillType; 1],
    >,
) {
    *counter += 1;

    if *counter == 1 {
        title.0 = "changed";
    }
}

let system = simulate.build(SkillType(3));
assert_eq!(
    system::Descriptor::get_spec(&system).debug_name.as_str(),
    "simulate[counter = 0, skill_id = SkillType(3)]"
);

{
    // We can also call the function directly in unit tests.

    let mut counter = 0;
    let mut title = Title("original");

    let mut world = dynec::system_test! {
        simulate.build(SkillType(2));
        _: Player = (
            PositionX(0.0),
            PositionY(0.0),
            Direction(0.5, 0.5),
        );
        _: Player = (
            PositionX(0.5),
            PositionY(0.5),
            Direction(0.5, 0.5),
        );
    };

    simulate::call(
        &mut counter,
        &SkillType(2),
        &mut title,
        world.components.write_simple_storage(),
        world.components.write_simple_storage(),
        world.components.read_simple_storage(),
        world.components.read_partial_isotope_storage(
            &[SkillType(3)],
            world.ealloc_map.snapshot::<Player>(),
        ),
    );

    assert_eq!(counter, 1);
    assert_eq!(title.0, "changed");
}