Design Patterns - GoldhawkInteractive/X2-Modding GitHub Wiki
The strengths of Artitas only truly come to light if the user embraces reactive design. Within Artitas this comes in two forms:
- Reacting to a change in an Entity.
- React to a Component added or removed.
- React to a change in a Component variable.
- React to an Entity entering or leaving a Family.
- Reacting to an event in the world.
WARNING
Adding or Removing a Component inside a ComponentAdd/Update/Remove is delayed. This is done because if we don't, the entity might become an illegal member of a family.
These changes are delayed until all listeners are called that want to be notified of a change.
The reason for this is that all listeners are called after a mutation on an Entity. This is done because the state of the Entity needs to be guaranteed to conform to the constraints placed on it for the listeners. In other words: If you add a listener to a Family which requires components A,B & C, the system will enforce that an entity has those components when listeners attached to the family are called. If another listener were to be allowed to remove component A, then any listener coming after it would need to check whether A still existed on the Entity.
All listeners dealing with changes to an Entity
are attached to a Family
.
The first step in reacting to any change on an Entity
is defining this Family
, which you can read up on on the Family page. The following image shows an overview of which events you can respond to, and when they will occur:
Once an Entity
is in a Family
, any changes to its Components will trigger Family.ComponentUpdated
. This includes the Component
that first allowed the Entity
to enter the family. There are three types of events associated with Components on Entities. All of these can trigger either on all Components changes, or changes to components of a certain type:
-
Family.ComponentUpdate
: Triggered whenever aComponent
is added or removed, along with the previous and new value. This also triggers when theEntity
previously had noComponent
. -
Family.ComponentAdd
: Triggered whenever aComponent
is added or replaced with the new value as a parameter. -
Family.ComponentRemove
: Triggered whenever aComponent
is removed or replaced with the previous value as a parameter.
Reacting to a change in a Component
variable is, in actuality, reacting to a Component
being replaced. The idea is that we leverage the DSL generated by Artitas to trigger these updates in a performant manner.
The Artitas Code Generator generates DSL to add and instantiate Components based on their public variables. The DSL uses a pool to avoid instantiating components, returning components which are deleted to the Pool.
By using the generated DSL to change a variable we can trigger Family.ComponentUpdate
which acts, for all intents and purposes, as a value update.
Per example, let's say we have a Component Health, with a variable int "value", we can create code that listens to a decrease like such:
public class Health : IComponent {
public int value;
}
Family health = world.RegisterFamily(new Family(Matcher.All<Health>()))
.ComponentUpdate<Health>((family, entity, previousHealth, nextHealth) => {
if(previousHealth.value < nextHealth.value)
Debug.WriteLine("Help, this unit is dying!");
});
Entity poorUnit = world.CreateEntity()
.AddHealth(10) // Created with code generation
poorUnit.AddHealth(8) // Prints: "Help, this unit is dying!"
While the overhead on this is as small as possible, it still exists. If you access and change the variables in a Component
directly, no changes will trigger in the Family
. You can abuse this behaviour to control when the change is triggered by manually triggering the Component
add yourself with the current Component
, per example:
Health health = entity.Health();
health.value = 5;
entity.AddComponent(health) // Triggers the Component Update.
However, the age old adage holds:
Premature optimization is the root of all evil.
It is best to use the default change and only bypass the event trigger once it becomes apparent the code needs to be optimized.
To react to an Entity
entering or leaving a Family
you can simply subscribe to Family.EntityAdded
or Family.EntityRemoved
. An Entity
automatically enters a Family
once matched on both the Family's Matcher
and Filter
.
The base pattern for a System
to respond to an Event
in the World
is to subscribe to the event, and then handle it in the Handle(IEvent)
method of the System.
public class RenderSystem : BaseSystem {
public override void OnInitialize() {
SubscribeTo<DeltaTimeEvent>();
}
public override void Handle(IEvent trigger) {
if (trigger is DeltaTimeEvent) {
var dtEvent = (DeltaTimeEvent) trigger;
// Execute code using DeltaTimeEvent
}
}
}
As subscribing to these events and then casting them can become quite tedious, the EventSystem
was implemented. It provides the SubscriberAttribute
which allows you automatically subscribe and hookup all methods receiving an Event
. Simply inherit from EventSystem
, and make sure that any receiving methods are public method and accept a single Event
argument annotated with Subscriber
. The System
will then automatically subscribe them to the correct event, and hookup the dispatch. By convention, the method that accepts the event should be named after its intent, i.e.: what it does in response to the event.
public class RenderSystem : EventSystem {
[Subscriber]
public override void RenderFrame(DeltaTimeEvent trigger) {
// Execute code using DeltaTimeEvent
}
}