Advanced Patterns - GoldhawkInteractive/X2-Modding GitHub Wiki

Archetypes and Families

Archetypes are implicitly convertible to and from Matchers. This is particularly handy when creating Families based on them. It gives you safety when you alter the Archetype and makes the definition of the Family easier to read.

public class GameArchetypes {

   public static Archetype Player = Matcher.All<PlayerComponent>();

}

var players = World.RegisterFamily(GameArchetypes.Player);

Event Types

Typically Events can easily be distinguished into two separate groups: Commands and Reports. A Command event typically instructs the game to execute some logic, while the Report signals its outcome. Internally we've found it handy to use this naming in the class name and even use it in the class hierarchy, so that we can filter on each one respectively. Per example:

// An Action is defined as any move or attack done by a entity in this game
public abstract class ActionCommand : Event {}
public abstract class ActionReport : Event {}

// Orders a unit to move to a certain location
public class MoveToCommand : ActionCommand {}

// Thrown to indicate that the Unit has started or finished movement.
public class MovementStartedReport : ActionReport {}
public class MovementFinishedReport : ActionReport {}

Using the above structure, one could implement an OverwatchSystem that listen to all ActionReport events to decide whether it needs to trigger.

Overriddable logic using Events or Boomerang Events

All Events have a Consumed variable, which any System can set to true during its handling to ensure the Event is not forwarded to any Systems coming after it. This allows us to implement a pattern in which a System throws an Event in the World and subscribes to it itself, executing some logic that can then be overridden by other Systems.

So if the event does enter the System, it executes the default logic. However, any System added to the World that subscribes to the event, can consume it to block the default logic from executing. This way you can implement priority-based execution of some logic. (Think default clean-up behavior, gates of logic on whether the default logic may execute, etc)

A more concrete example would be using the above ActionReport again. Let's say we define an AISystem that in response to the ActionReport by any AI unit will issue the next command to the AI. We could implement an OverwatchSystem that listens to ActionReports and consumes them if it triggers an Overwatch, ensuring the AI still remains in a halted state. After execution it would then give a signal to the AI that it could start again.

This pattern can be implemented in various ways:

HandleEvent based

Create an event and hold a reference to it, throw it in the World and see if any System has responded to it. Use this pattern if you don't want to wait until the World executes the queue till you receive your results.

var movementReport = new MovementEndedReport(entity, location);

World.HandleEvent(movementReport) // Immediately delegates this event to all subscribing Systems

if(movementReport.Consumed)       // Consumed is defined by default, to be more expressive, one can define a specific field in the Event, e.g.: MovementShouldStop
    StopMovement();

Subscription based

Subscribe to the event, and ensure that the System it receives events last. This can be done by adding the System last to the World. This pattern is useful if you don't immediately need to execute the task and can wait on the World till the event is executed.

public void Execute(){
    ...
    World.HandleEvent(new OverwatchEvent());
    ... 
}

[Subscriber]
public void Cleanup(OverwatchEvent overwatch){
   // The event was not picked up/consumed by anyone, so clean it up.
   overwatch.entity.Delete();
}

Piggybacking on Events (Decorator pattern)

Given the above pattern, one could implement a design that allows any Systems to subscribe to an Event and alter it. The altered event is then used to execute some logic. A more concrete example of this would be in an RPG game where an attack is made on another entity. Per example:

  1. Define a DamageSystem, it creates a DamageEvent which does some base damage on an Entity, and throws the event in the World using HandleEvent().
  2. Subscribe other systems to the DamageEvent, that add damage to this DamageEvent based on logic. E.g.: ResistanceSystem which reduces the damage done by a DamageEvent based on the resistance of an entity, or EnvironmentDamageSystem which adds damage done by the environment.
  3. The DamageSystem sees how much damage was done by the event, and applies it to the entity.
var damage = new DamageEvent(entity, value);

World.HandleEvent(damage)   // Both ResistanceSystem and EnvironmentDamageSystem add/subtract their respective values from the event

entity.DeltaHealth(-damage.value);
⚠️ **GitHub.com Fallback** ⚠️