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.
- 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 useFamily
. When defining logic outsideBaseSystem
or when the overhead of defining and maintaining aFamily
, the direct lookup inRelationshipManager
can be used instead of going over all entities and filtering.World
implements delegation methods (FindTagged()
,FindGroup()
) to theRelationshipManager
to ease use.
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.
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.
- Querying the
RelationShipManager
inWorld
, throughworld.RelationshipManager.FindTagged(..)
. - Alternatively, for static tags, by registering a
Family
that usesMatcher.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.
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.
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.
- Querying the
RelationShipManager
inWorld
, throughworld.RelationshipManager.FindGroup(..)
. - Alternatively, for static groups, by registering a
Family
that usesMatcher.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.
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>{}
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 componentFoo
which links to "other", and "other" will have a componentBar
which contains a reference to "entity". -
entity.DetachFromFoo()
: Detaches "entity" from any entity it might be linked, removingFoo
from "entity" andBar
from any entity that "entity" is currently connected to. -
entity.IsLinkedToFoo(Entity other)
: Returns whether this entity is linked to "other" usingFoo
.
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
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