Relationships - GoldhawkInteractive/X2-Modding GitHub Wiki

Relationships in Artitas define links between entities and other entities, strings or types. Currently, four different relationships are defined within Artitas:

  • Tag: A link between a single, unique entity and a string or Type.
  • Group: A link between multiple entities and a string or Type.
  • One-to-(One/Many): A link between a single entity and another, or group of entities.
  • Cascade: A link between a single Entity (author) and a group of entities (subscribers) in which all changes on the author propogate to the subscribers.

By using relationships, you gain several benefits:

  • Each type of relationship defines specific semantics that are maintained for you and enables you to program reactively on them. (E.g.: One-to-X will maintain and update the other side of a relationship if one side is change. Per example, linking a Soldier to a Weapon will ensure that the Weapon gets a Component indicating the link to the Soldier as well, removing the link from Soldier will remove it from Weapon as well.)
  • Each type of relationship defines DSL specific to that relationship, to better showcase their intent. (E.g. for Group "Soldiers": JoinSoldiers(), LeaveSoldiers(), IsMemberOfSoldiers())
  • Formalizing these concepts results in clearer code, and code that you can programmatically approach. (E.g.: All Group components will inherit from IGroupComponent)

The RelationshipManager in the World serves as the system that oversees the registration, maintenance of relationships and general entry point in logic on relationships.

Best practices

  • Put relationships of a type together in the same file. Relationships tend to be easy to define, so define them together. (E.g.: Links.cs, Groups.cs, etc)
  • Use Tags to denote/replace global state. While ideally we would remove it all together, there are cases where we need to define a single or unique entity. Per example which Players turn it currently is.
  • Use Group to denote a certain set of Entities for debug purposes. If you have well defined entities with specific sets of components, add a Group Component to mark the Entity type. It tends to lead to easier debugging as it becomes easy to see what type of Entity you are dealing with.
  • Refrain from using the Group Component in Family filtering unless necessary. If you filter on the GroupComponent you are no longer filtering on the capability of an entity, but on its designation. This goes against the principle of good ECS design and is a step back to Object Oriented design where a Type, not its capabilities (Components) represents its functionality.
  • Use RelationshipManager for lookup when you can't use Family. When defining logic outside BaseSystem or when the overhead of defining and maintaining a Family, the direct lookup in RelationshipManager can be used instead of going over all entities and filtering. World implements delegation methods (FindTagged(), FindGroup()) to the RelationshipManager to ease use.

Tag

The Tag relationship serves as a replacement for global state and is represented by the ITagComponent. Use this relationship if you need to enforce that only a single component of a type exists, or want to quickly access a specific entity in the world.

It links an entity to a tag in the form of either a string (dynamic) or a Component Type (static), and enforces that only a single entity may be linked to it. When a new entity registers itself to a tag, it will replace the previous (if any) entity linked to the tag.

Assigning or changing a Tag

The Tag relationship defines the following DSL for static tags, where Foo represents the name of the type:

  • entity.TagAsFoo(): Assigns the Foo tag to the given entity, removing the tag from a previously assigned entity.
  • entity.UntagAsFoo(): Removes the Foo tag from the given entity.
  • entity.IsTaggedAsFoo(): Returns whether the given entity is tagged as Foo or not.
Finding an entity assigned to a tag.
  • Querying the RelationShipManager in World, through world.RelationshipManager.FindTagged(..).
  • Alternatively, for static tags, by registering a Family that uses Matcher.all<{Tag}>.

StaticTag

The most common use of the Tag relationship is by defining a specific Component as a tag. This can be done either through inheriting from ITagComponent or StaticTag. StaticTag serves as a shorthand to be used if the component only represents the TagRelationship and will implement the Copy() function for you.

E.g.: "CurrentPlayer" Tag Relationship

// Naive definition
public class CurrentPlayer : ITagComponent {
    public IComponent Copy(){
       return new CurrentPlayer();
    }
}

// Or shorter, but semantically same as above
public class CurrentPlayer : StaticTag<CurrentPlayer> {}

// Generated Syntax:
entity.TagAsCurrentPlayer();
entity.UntagAsCurrentPlayer();
entity.IsTaggedAsCurrentPlayer();

DynamicTag

The dynamic tag allows you to associate an entity with an arbitrary string. Instead of attaching a specific ITagComponent type to an entity, using the DSL (entity.TagAs(string)) will attach a DynamicTagsComponent to your entity that will hold all tags this entity is associated with. The DSL for dynamic tags conforms to the static variant, is part of the framework and therefore does not need to be generated.

Group

The Group relationship is used to define a set of entities which belong together, grouped under a string or Component Type.

The benefits of using this over just a Component is that it allows you to efficiently access this list of entities through the RelationshipManager on locations where you cannot use a Family. It furthermore helps to distinguish which components are used to just group entities, and those that provide actual state and functionality.

Joining or leaving a Group

The Group relationship defines the following DSL for static groups, where Foo represents the name of the type:

  • entity.JoinFoo(): Adds the entity to the Foo group.
  • entity.LeaveFoo(): Removes the entity from group Foo.
  • entity.IsMemberOfFoo(): Returns whether the given entity is in the Foo group.
Finding all entities in a group.
  • Querying the RelationShipManager in World, through world.RelationshipManager.FindGroup(..).
  • Alternatively, for static groups, by registering a Family that uses Matcher.all<{Group}>.

StaticGroup

The definition and DSL of a Group relationship works similar to the Tag Relationship, and can be done either through inheriting from IGroupComponent or StaticGroup.

E.g.: "Soldiers" Group Relationship

// Naive definition
public class Soldiers : ITagComponent {
    public IComponent Copy(){
       return new Soldiers();
    }
}

// Or shorter, but semantically same as above
public class Soldiers : StaticGroup<Soldiers> {}

// Generated Syntax:
entity.JoinSoldiers();
entity.LeaveSoldiers();
entity.IsMemberOfSoldiers();

DynamicGroup

The dynamic group allows you to associate an entity with an arbitrary string and is defined and used similarly as the DynamicTag.

One-to-X

The One-to-X relationships create a link between entities that can be accessed, and are maintained, from both sides. Currently, the framework supports One-to-One and One-to-Many relationships. No support is planned for Many-to-Many relationships at the time of writing.

To define a relationship, define two component classes and let them inherit from either OneLink, for the component that represents a single entity or ManyLink, for the component that represents multiple entities. You then add the [LinkedTo] attribute to both components and point them at the other side of the relationship. Per example, to define a Parent <> Child relationship:

[LinkedTo(typeof(Child)] // So the World knows that Child is the other side of this relationship
public class Parent : OneLink<Parent>{} // OneLink inherits from Component, so Parent is a Component.

[LinkedTo(typeof(Parent)]
public class Child : OneLink<Child>{}
Linking to another Entity, or removing a link.

The One-to-X relationship defines the following DSL, where Foo and Bar represent two sides of a link:

  • entity.LinkToFoo(Entity other): Links the entity to the given entity "other". The result is that "entity" will have a component Foo which links to "other", and "other" will have a component Bar which contains a reference to "entity".
  • entity.DetachFromFoo(): Detaches "entity" from any entity it might be linked, removing Foo from "entity" and Bar from any entity that "entity" is currently connected to.
  • entity.IsLinkedToFoo(Entity other): Returns whether this entity is linked to "other" using Foo.
Examples

To fully understand this type of relationship this sections provides some more details and tips on how to use it:

Examples of One-to-X relationships:

  • One-to-One
    • Parent <> Child
    • Husband <> Wife
    • Hero <> ArchEnemy
    • Disease <> Cure
    • PowerSocket <> Appliance
  • One-to-Many
    • Parent <> Children
    • Team <> Players
    • Boss <> Employees
    • MindController <> MindControlling

To avoid confusion, it's a good habit to name the type after its arity. As shown above, Child points at a single child, while Children indicates that it points at multiple children.

Also, it's best to use the relationships much as you would use and name the variables they actually indicate. So, the ParentComponent will hold a reference to an Entity which will be the Parent of the entity with that component. To make this a bit more clear, let's give a proper example using the Parent <> Child relationship:

Entity child = world.createEntity();
Entity parent = world.createEntity();

// Create the link between the entities.
child.LinkToParent(parent);

bool hasParent = child.hasParent(); // True, child will now have a ParentComponent.
bool isLinkedToParent = child.IsLinkedToParent(parent); // True, parent is stored in ParentComponent.

// As the world will also update the otherside of the relationship:
bool hasChild = parent.hasChild(); // True
bool isLinkedToChild = parent.IsLinkedToChild(child); // True

Cascade

The last relationship provided by the Artitas is the Cascade relationship. It provides a way to automatically propagate changes (adding or removing components) on one entity (author) to a set of listening entities (subscribers). Components added to the author are copied into the subscribers.

To subscribe an entity to any changes of another entity, the following will be enough:

Entity author = world.CreateEntity();
Entity subscriber = world.CreateEntity();

subscriber.SubscribeTo(author);  // or author.CascadeTo(subscriber)

// Now, if we add a component to author, any subscriber will have a copy of it as well.
author.AddComponent(new Health());
bool hasHealth = subscriber.HasComponent<Health>(); // True

The cascade operation can also be restricted to only propagating certain components. To do this, add the CascadeSubscribersComponent on the author with the types you want to propagate, per example:

Entity author = world.CreateEntity();
Entity subscriber = world.CreateEntity();

author.AddCascadeSubscribers(typeof(HealthComponent)); // Now only Health will propagate
subscriber.SubscribeTo(author);

author.AddComponent(new Health());
bool hasHealth = subscriber.HasComponent<Health>(); // True

author.AddComponent(new Mana());
bool hasMana = subscriber.HasComponent<Mana>(); // False, as it didn't propagate
⚠️ **GitHub.com Fallback** ⚠️