Design Patterns - GoldhawkInteractive/X2-Modding GitHub Wiki

Reactive Design

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.

Reacting to a Change in 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:

lifecycle

React to a Component Being Added or Removed

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 a Component is added or removed, along with the previous and new value. This also triggers when the Entity previously had no Component.
  • Family.ComponentAdd: Triggered whenever a Component is added or replaced with the new value as a parameter.
  • Family.ComponentRemove: Triggered whenever a Component is removed or replaced with the previous value as a parameter.

React to a Change in a Component Variable

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.

React to an Entity Entering or Leaving a Family

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.

Reacting to an Event in the World

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
	}
}
⚠️ **GitHub.com Fallback** ⚠️