Archetype - GoldhawkInteractive/X2-Modding GitHub Wiki
Archetypes are a core concept in our ECS (Entity-Component System) that group components into named classes, traits, and states. An archetype defines a specific set of components (and sometimes component values) that an entity can conform to. In practice, an archetype serves both as a query definition (to find all entities with those components) and as a template or source for creating entities with a predefined composition. This approach provides a clear structure, making it obvious what components an entity should or should not have, and avoids the typical ECS pitfall of ad-hoc combinations of components. It also enables a form of functional reactive programming: systems subscribe to archetype-based queries and react when entities enter or leave those archetypes (by adding/removing components or changing component data).
An Archetype can be defined by the Matcher
it conforms to and any default values that should be set for Components when an Entity is constructed. It supports single-parent inheritance that works as expected.
public static Archetype Unit = Matcher.All<HealthComponent, ManaComponent>();
public static Archetype Soldier = Archetype.BaseOn(Unit)
.ConformingTo(Matcher.All<RankComponent>());
Class-like archetypes represent a concrete, fully-defined "class" of entity, much like a class in OOP. A class archetype is usually named as a noun and defines all the components (with default values) required for that entity type. Entities typically conform to exactly one class archetype. Class archetypes often use inheritance (one archetype BaseOn another) to share common setups.
-
Definition: A class archetype outlines a complete set of components and their initial values for a specific kind of entity. For example, the
GroundCombatArchetypes.Combatant
archetype defines a combat unit with all necessary components for combat logic (health, stats, inventory, etc.). Its documentation describes it as "all team-based participants of actual combat with specific statistics for game logic (visibility, bravery, reflexes, etc)". It builds on a more generalActor
archetype and then ConformsTo several trait archetypes (like shared combatant definitions, spawn logic, etc.) to include additional components. Finally, it provides AddDefault components to ensure sane initial values (e.g. a default name, default stats, etc.). -
Example – Combatant: The Combatant class archetype is a fully realized entity template for soldiers/aliens in ground combat. It inherits from a generic
Actor
archetype (adding basic physical presence and animation) and then adds combat-specific components. In the code,Combatant
is defined with components such asGCCombatantMetaComponent
,CombatantsGroup
(a grouping tag for all combatants),LifeStatusComponent
(for alive/unconscious state), and many others. It also conforms to trait archetypes likeMoraleTrait
andStressTrait
to grant those systems' functionality. A number of default components are added as well – for instance, a default emptyNameComponent
, a default rank, base accuracy, etc., to minimize setup when creating a new combatant. This ensures any entity spawned as aCombatant
starts with all the necessary data out-of-the-box. -
Other Class Examples: Another class-like archetype might be an
Armour
(not shown here) or a vehicle, which would similarly enumerate all the components that define that object. Class archetypes often include constant identifiers or taxonomy components to categorize the entity. They may also include group/tag components to mark the entity type; for example,Combatant
includes theCombatantsGroup
tag component to mark the entity as a combatant. (We occasionally use such group tags for convenience, predominantly in class definitions, but the archetype itself serves as the primary tag representing the concept.)
Key point: An entity usually has one class archetype, which acts as its fundamental identity. This makes it easy to know exactly which components every entity of that class should have, and you can search or query the game for all entities of that class via the archetype name.
Trait-like archetypes represent a modular concept or capability that an entity can have. They are usually described by adjectives or verbs (e.g. "Targetable", "Modifiable") indicating a quality or functionality. Traits are additive: an entity can conform to multiple trait archetypes simultaneously, on top of its class. They enable specific behaviors or system interactions without being a full entity definition on their own.
-
Definition: A trait archetype defines a set of components (often a minimal set or even just one marker component) that grants a particular functionality or property. Unlike class archetypes, traits typically don't encompass a whole entity's data, but rather one aspect of it. Traits often serve as tags or filters that systems use to include/exclude entities in certain logic.
-
Example –
IsTargetable
: This is a trait archetype used in ground combat to mark anything that can be targeted for an attack. It’s defined as “anything that is objectively targetable for a shot”. In code,GroundCombatArchetypes.IsTargetable
is specified by requiring anAddressComponent
(location on the board) and aHitPoints
component. Any entity (whether a unit, object, or prop) that has a position and health will conform toIsTargetable
, meaning targeting systems or AI can identify it as a valid target via this archetype. The archetype itself doesn't add new data; it’s a query definition that the entity already has those components, effectively acting as a tag for "targetable objects." -
Example –
IsSuppressedTrait
: In the suppression mechanics,GroundCombatArchetypes.IsSuppressedTrait
is a trait archetype indicating an entity is currently suppressed. The code defines it simply as requiring theSuppressedGroup
component on the entity. When the suppression system applies the Suppressed status to a combatant, it likely adds thisSuppressedGroup
tag component, causing the entity to conform toIsSuppressedTrait
. Systems (like AI or UI) can then query forIsSuppressedTrait
to find all suppressed units. In other words, suppression is modeled as a trait that can come and go by adding or removing theSuppressed
tag on the entity. -
Other Trait Examples: Common traits include things like
MoraleTrait
(entities that have morale tracking, defined by components like Morale and related data), orModifying
(from CommonArchetypes, which marks an entity that applies modifiers to others, defined by presence of a ModifiersComponent). Trait archetypes can also have default components if needed; for example, aModifying
trait adds a default emptyModifiersComponent
so the entity is ready to hold modifiers. Generally, though, traits are often just filters/tags and do not always inject a lot of data themselves.
Key point: An entity can have multiple traits to mix and match functionality. This modular design makes it clear which features an entity has by looking at which trait archetypes it conforms to. It also means systems can simply query “all entities with trait X” instead of checking raw components individually.
State-like archetypes represent a particular state or condition of an entity, often in relation to a class archetype. They answer the question "is the entity currently in state X?" For example, a combatant could be "Alive" or "Dead", a door could be "Open" or "Closed", an item could be "Destroyed" or "Intact". State archetypes are usually named as conditions (often prefixed with "Is" or similar) and are used primarily for querying and reacting to changes in that state. An entity's state archetypes can change over time as the game runs (unlike class which is fixed, and traits which are more static capabilities, states tend to be dynamic conditions).
-
Definition: A state archetype is essentially a filter that matches entities in a certain condition, often by looking at a component’s value or the presence/absence of a marker. State archetypes are tightly associated with either a class or a trait – they don't stand alone but rather indicate a variant or status of an entity of some type. An entity can conform to multiple state archetypes, but usually not ones that conflict (e.g. an entity might be "Damaged" and "Targetable" at the same time, but not "Dead" and "Alive" simultaneously for the same aspect).
-
Example –
IsDestroyed
: This archetype flags entities that have been destroyed. In the damage system, any entity that has aDamageStateComponent
can be in various damage states (undamaged, damaged, destroyed, etc.).GroundCombatArchetypes.IsDestroyed
is defined as a state filter: it requires the entity have aDamageStateComponent
and adds a condition that this component’s state is DESTROYED. In other words, when an entity’sDamageStateComponent
indicates it’s destroyed, the entity will conform toIsDestroyed
(and conversely, if repaired or not yet destroyed, it will not conform). This is used for queries like finding all destroyed objects (for loot generation, cleanup, or logging purposes). Similar state archetypes exist forIsDamaged
andIsUndamaged
, using the same component with different state checks. -
Example – Combatant Conscious/Unconscious: For combatant entities, the game might need to know who is still conscious. A state archetype
CombatantConscious
is defined as a variant of the Combatant class with a filter: it checks the combatant’sLifeStatusComponent
to see if the unit is conscious. If the life status indicates unconscious or dead, the entity would no longer match this Conscious archetype. This provides a clear way to query “all conscious combatants” in a system (e.g., to skip unconscious ones in turn order or AI logic). We could similarly have an archetype for a "Dead" or "Unconscious" state if needed, though in many cases the absence of the conscious state or a check onLifeStatusComponent
is enough for logic.
Key point: State archetypes let systems and designers refer to dynamic conditions in a declarative way. Instead of each system checking a component’s value manually, they can simply query for entities conforming to IsDestroyed
, IsEmptyForRecovery
, CombatantConscious
, etc. State archetypes are typically controlled by systems (for example, the Health system might add/remove a Suppressed component or update a DamageState, thereby causing the entity to enter or leave these state archetypes automatically).
One of the strengths of this approach is that you combine a class archetype with multiple traits (and experience various states) to form a complete entity. Think of a class archetype as the base template, and trait archetypes as mix-ins that add capabilities, while state archetypes reflect the current situation of that entity.
-
Example – Combatant with Traits: A Combatant entity in Ground Combat will inherently conform to the
Combatant
class archetype (giving it all core components). On creation, it also immediately conforms to several trait archetypes because the class definition included them. For instance, as seen above,Combatant
conforms toMoraleTrait
,StressTrait
, andSuppressibleAssetTrait
. This means a combatant has morale, can experience stress, and can be suppressed. It will also conform toIsTargetable
by virtue of having an address and hitpoints (so you don’t even need to manually addIsTargetable
; the presence of those components makes it targetable automatically). If the combatant is an AI-controlled unit, it might additionally satisfyIsAICombatant
(which filters combatants by their controller link). All these trait archetypes layer on top of the core class. -
Dynamic State Changes: Continuing the Combatant example, during gameplay the entity might enter certain state archetypes. If the unit becomes suppressed by enemy fire, a
SuppressedGroup
component is added, and now the entity matchesIsSuppressedTrait
. If it later recovers (the component is removed), it no longer matches that archetype. If the unit takes lethal damage, itsDamageStateComponent
might be marked DESTROYED, so it now conforms toIsDestroyed
(and presumably would cease to conform to a "conscious" or "alive" archetype). These changes are tracked automatically by the ECS – as components are added/removed or values change, the set of archetypes an entity conforms to is updated. Systems can subscribe to those changes (reactive paradigm), ensuring game logic responds appropriately (e.g., the loot system might look for all entities withIsDestroyed
andHasRecoveryLootTable
to generate loot from destroyed entities). -
Archetype as Tag vs Component Tag: Rather than relying purely on raw tag components all over, we use archetypes as the semantic tags. For example, instead of checking for a boolean or empty component "IsTargetable" on each entity, we define the
IsTargetable
archetype (which implicitly tags any entity meeting its criteria). This makes the code and data easier to understand – you ask "Is this entity targetable?" by seeing if it conforms to theIsTargetable
archetype. In some cases we do use marker components (like groups) internally, but usually as part of an archetype definition. The archetype names provide a higher-level vocabulary for entity concepts. -
Documentation and Conventions: Each archetype is documented in the code with its intended usage. The documentation often mentions related systems or behaviors triggered by that archetype. For instance, the
ManageAddress
archetype (in CommonArchetypes) notes that it ensures anAddress
is calculated for entities on the board and mentions that the address is removed if the entity no longer conforms – this implies a system (the BoardSystem) is listening for entities withManageAddress
and updating their AddressComponent. We follow some general conventions for defining archetypes:- Naming: Use nouns for class archetypes (e.g. Combatant), adjectives/verbs for traits (e.g. IsTargetable, Modifying), and state-like phrasing for states (e.g. IsDestroyed).
- Single Class: An entity should have only one class archetype, but can have multiple traits.
-
Defaults: Archetypes can specify default component values. These sane defaults help minimize the data needed to set up entities. For example,
Combatant
auto-fills many stats so a new unit isn’t missing critical components. When defaults are non-obvious, the archetype’s doc will explain why they are set. - Systems Linking: If an archetype’s presence is tied to a particular game system, the documentation will reference it (e.g., an archetype managing board presence might mention the BoardSystem that implements this logic). This helps modders and developers understand the implications of adding/removing that archetype.
- Constants & IDs: Some archetypes include constant identifiers (like taxonomy IDs or GUID components) to ensure the entity is uniquely identifiable or classified in save data. These constants are part of the archetype definition when needed for the game’s data model.
By using class, trait, and state archetypes, we achieve a balance between flexibility and structure in the ECS. You can compose entities from reusable pieces (traits) while retaining clear, OOP-like entity definitions (classes) and well-defined states. The result is easier debugging, more readable game logic (since queries are about meaningful archetypes rather than raw components), and a mod-friendly way to think about game entities. Modders don’t create new archetypes, but understanding them is crucial: when you see an archetype name in the data or code, it concisely tells you what that entity is or what it can do. And when you want to modify an entity’s behavior or check a condition, using the archetype vocabulary ensures you’re hooking into the game’s logic in the intended way.
In summary, an Archetype is both a query (to find entities with a set of components) and a template (to spawn or configure entities with those components). Class archetypes define what an entity is, trait archetypes define what an entity can do or has, and state archetypes define how an entity is currently. Through their combination, the system remains robust, clear, and extensible. Each archetype serves as a self-documenting tag that keeps the game logic clean and helps everyone (developers and modders alike) reason about entity behavior at a high level.
The order in which Components
are added to an entity is defined by the order in which Components are given to the Matcher. (Matcher.All{A,B,C}
will ensure that first A
, then B
and finally C
are added to the Entity).
The Code Generation will scan the entire codebase for any archetypes defined in public static readonly fields and create factory methods which will create either a Template
, or setup an Entity
.
The following Archetype:
public static Archetype Unit = Archetype.ConformingTo(Matcher.All<HealthComponent, ManaComponent>())
.AddDefault(new HealthComponent (50));
will generate a factory method on both Template
and Entity
, that can be used in the following ways:
Template.CreateUnit(10); // Creates a Unit with 50 Health and 10 Mana. Health is an optional value.
Template.CreateUnit(10, 5); // Creates a Unit with 10 Mana and 5 Health. (Optional values are last)
Template.CreateUnit(mana: 10, health: 5); // Named arguments based on the component names.
For each Archetype that is defined, an Attribute is created according to the name of its declaring field. These attributes point back at the Archetype and provide documentation on the intent and components defined within. They are mainly to be used throughout the codebase to signal which type of Entity is accepted or passed along.
A planned feature is to extend Visual Studio to provide Intellisense on only those components defined in the Archetype for any Entity annotated with such an attribute.
Per example, the above defined Archetype
"Unit" will create the following Attribute:
[Unit] Entity enemy;
public void DoDamage([Unit] Entity enemy){}
Furthermore, HTML diagrams which show all archetypes in a project (Per class they are defined in) are generated into the Assets folder of the CodeGenerator.