Static ECS - C# Entity component system framework - Unity module

May 1, 2026 · View on GitHub

Static ECS

English Русский 中文

Version Documentation Core framework Showcase

Static ECS - C# Entity component system framework - Unity module

Table of Contents

Contacts

Installation

Must also be installed StaticEcs

  • As source code

    From the release page or as an archive from the branch. In the master branch there is a stable tested version
  • Installation for Unity

    • As a git module in Unity PackageManager
      https://github.com/Felid-Force-Studios/StaticEcs-Unity.git
    • Or adding to the manifest Packages/manifest.json
      "com.felid-force-studios.static-ecs-unity": "https://github.com/Felid-Force-Studios/StaticEcs-Unity.git"

Guide

The module provides additional integration options with the Unity engine:

Connection:

ECS runtime data monitoring and management window
To connect worlds and systems to the editor window it is necessary to call a special method when initializing the world and systems
specifying the world or systems required

        ClientWorld.Create(WorldConfig.Default());
        ClientSystems.Create();
        
        EcsDebug<ClientWorldType>.AddWorld<ClientSystemsType>();
        
        ClientWorld.Initialize();
        ClientSystems.Initialize();

All system groups created within the world automatically appear in the debug window — no extra registration is required:

        ClientWorld.Create(WorldConfig.Default());
        ClientSystems.Create();
        ClientAdditionalSystems.Create();
        
        EcsDebug<ClientWorldType>.AddWorld<ClientSystemsType>();
        
        ClientWorld.Initialize();
        ClientSystems.Initialize();
        ClientAdditionalSystems.Initialize();

Note: AddWorld must be called before Initialize (it registers the debug system). The Systems tab discovers all Systems<TSystemsType> pipelines via the world handle.

Entity providers:

A script that adds the ability to configure an entity in the Unity editor and automatically create it in the ECS world
Add the StaticEcsEntityProvider script to an object in the scene:

EntityProvider.png

Usage type - creation type, automatically when Start(), Awake() is called, or manually
On create type - action after creating the provider, delete the StaticEcsEntityProvider component from the object, delete the entire object, or nothing
On destroy type - action when destroying the provider, destroy the entity or nothing
Prefab - allows referring to the provider prefab, while changing component data will be blocked
Entity GID - global entity identifier in runtime
Disable entity on create - disables entity immediately after creation
On enable and disable - enables/disables entity when GameObject is enabled/disabled
Entity Type - entity type
Cluster ID - cluster identifier in runtime
Components - component data
Tags - entity tags

Event providers:

A script that adds the ability to configure an event in the Unity editor and automatically send it to the ECS world
Add the StaticEcsEventProvider script to an object in the scene:

EventProvider.png

Usage type - creation type, automatically when Start(), Awake() is called, or manually
On create type - action after creation, delete the StaticEcsEventProvider component from the object, delete the entire object, or nothing
World - type of world in which the event will be sent
Type - event type

Unity event providers:

A system of providers for automatic forwarding of Unity events (physics, GUI) to the ECS world
Providers intercept Unity callbacks (OnCollisionEnter, IPointerClickHandler, etc.) and send corresponding ECS events

Type registration

Before using providers, you must register event and component types in the world
Call UnityEventTypes.Register<TWorld>() between Create() and Initialize():

        ClientWorld.Create(WorldConfig.Default());
        UnityEventTypes.Register<ClientWorldType>(); // Registers all events and components
        ClientWorld.Initialize();

The method automatically registers all module event and component types, respecting installed modules (Physics, Physics2D, TextMeshPro)

Alternatively, you can use RegisterAll to automatically register all types from specified assemblies via reflection:

        ClientWorld.Create(WorldConfig.Default());
        ClientWorld.Types().RegisterAll(typeof(MyComponent).Assembly); // All IComponent, IEvent, ITag from assembly
        ClientWorld.Initialize();

RegisterAll without parameters uses the calling assembly. Multiple assemblies can be passed

Three operating modes

ModeClass suffixDescription
Without entityProvider<TWorld>Sends event to the ECS world without entity binding
EntityGIDEntityGIDProvider<TWorld>Binds to entity via EntityGID field
EntityRefEntityRefProvider<TWorld, TProvider>Binds via reference to StaticEcsEntityProvider

Mode without entity - sends the event without EntityGID. Suitable for objects not associated with ECS entities

EntityGID mode - stores EntityGID as a serializable field. Sends event with EntityGID

EntityRef mode - stores a reference to a StaticEcsEntityProvider component. Gets EntityGID from the provider at runtime. Convenient when the entity provider is on the same or adjacent object

EntityEventMode

Entity providers (EntityGID/EntityRef) have a configurable EntityEventMode field in the Inspector that controls behavior:

ModeDescription
AllSends events and manages state components
EventOnlyOnly sends ECS events, without component management (default)
ComponentOnlyOnly manages state components on the entity, without sending events

Setting from code:

GetComponent<MyCollision3DEntityGID>().SetEntityEventMode(EntityEventMode.All);

Generating providers

Since Unity does not support serialization of open generic types, you need to create sealed subclasses for a specific world

For quick generation use: Assets/Create/Static ECS/Providers

The generation window allows you to configure:

  • World - the world type for which providers are generated
  • Namespace - namespace for generated classes
  • Prefix - class name prefix (defaults to world name)
  • Event providers - checkboxes for selecting event categories:
    • GUI - basic GUI events (Click, Drag, Drop, Pointer, ScrollView, Slider, SubmitCancel, ButtonClick)
    • TextMeshPro - TMP widget events (Dropdown, Input)
    • Mouse - mouse events (MouseDownUp, MouseEnterExit, MouseUpAsButton)
    • Physics 3D - 3D physics events (Collision, Trigger, ControllerColliderHit)
    • Physics 2D - 2D physics events (Collision, Trigger)
    • Animation - animation events (AnimationEvent, StateMachineBehaviour, StateMachineBehaviourLinker)

As a result, sealed classes ready to use in Unity Inspector will be generated
For each event type, 3 classes are generated (one for each mode):

{Prefix}Click          : ClickProvider<TWorld>              // without entity
{Prefix}ClickEntityGID : ClickEntityGIDProvider<TWorld>     // with EntityGID
{Prefix}ClickEntityRef : ClickEntityRefProvider<TWorld, TProvider> // with provider

Supported events

Physics 3D (requires com.unity.modules.physics):

ProviderUnity CallbackEventsState component
Collision3DOnCollisionEnter/ExitCollisionEnter3DEvent, CollisionExit3DEventCollision3DState
Trigger3DOnTriggerEnter/ExitTriggerEnter3DEvent, TriggerExit3DEventTrigger3DState
ControllerColliderHit3DOnControllerColliderHitControllerColliderHit3DEvent-

Physics 3D — ContactEvent (requires com.unity.modules.physics, Unity 2022.2+):

Centralized high-performance alternative to per-object MonoBehaviour callbacks. Uses Physics.ContactEvent — a batch callback receiving all contacts in the scene in a single call. Does not require a MonoBehaviour on every physics object.

Architecture:

  • Listener — one per scene. Subscribes to Physics.ContactEvent and processes all contacts centrally
  • ContactColliderProvider — on each GameObject with colliders (entity variant only). Registers colliders in the mapping

Two listener variants:

ListenerEventsState componentEntity mapping
ContactEventListener<TWorld>ContactEnter3DEvent, ContactExit3DEvent-Not required
ContactEventEntityListener<TWorld>ContactEnter3DEntityEvent, ContactExit3DEntityEventContactCollision3DStateVia ContactColliderEntityMap

ContactEventListener<TWorld> — sends events with collider data only, without entity binding. One component on a scene manager object.

ContactEventEntityListener<TWorld> — sends events with EntityGID of both bodies, using ContactColliderEntityMap world resource for Collider InstanceID → EntityGID mapping. One component on a scene manager object. Options: sendNonEntityEvents, manageComponents.

Collider registration (entity variant only):

ContactColliderProvider<TWorld> — MonoBehaviour placed on each GameObject with colliders. Registers colliders in ContactColliderEntityMap and automatically sets providesContacts = true. Two variants:

  • ContactColliderGIDProvider<TWorld> — serialized EntityGID
  • ContactColliderRefProvider<TWorld, TProvider> — reference to StaticEcsEntityProvider

The ContactColliderEntityMap resource is created lazily on first provider registration.

Contact events contain data about both colliders:

public struct ContactEnter3DEntityEvent : IEvent {
    public EntityGID EntityA;
    public EntityGID EntityB;
    public Collider ColliderA;
    public Collider ColliderB;
    public Vector3 Point;
    public Vector3 Normal;
    public Vector3 Impulse;
}

Note: Physics.ContactEvent only reports actual collisions, not triggers. For triggers, use the standard Trigger3D providers.

Physics 2D (requires com.unity.modules.physics2d):

ProviderUnity CallbackEventsState component
Collision2DOnCollisionEnter2D/Exit2DCollisionEnter2DEvent, CollisionExit2DEventCollision2DState
Trigger2DOnTriggerEnter2D/Exit2DTriggerEnter2DEvent, TriggerExit2DEventTrigger2DState

GUI:

ProviderUnity InterfaceEventsState component
ClickIPointerClickHandlerClickEvent-
PointerEnterExitIPointerEnter/ExitHandlerPointerEnterEvent, PointerExitEventPointerHoverState
PointerUpDownIPointerUp/DownHandlerPointerUpEvent, PointerDownEventPointerPressedState
DragIBeginDrag/IDrag/IEndDragHandlerDragStartEvent, DragMoveEvent, DragEndEventDragState
DropIDropHandlerDropEvent-
ScrollViewScrollRect.onValueChangedScrollViewChangeEvent-
SliderChangeSlider.onValueChangedSliderChangeEvent-
SubmitCancelISubmitHandler/ICancelHandlerSubmitEvent, CancelEvent-
ButtonClickButton.onClickButtonClickEvent-

GUI TMP (requires com.unity.textmeshpro or com.unity.ugui >= 2.0.0):

ProviderUnity CallbackEvents
DropdownChangeTMP_Dropdown.onValueChangedDropdownChangeEvent
InputChangeTMP_InputField.onValueChangedInputChangeEvent
InputEndTMP_InputField.onEndEditInputEndEvent

Mouse (requires a collider on the object):

ProviderUnity CallbackEventsState component
MouseDownUpOnMouseDown/UpMouseDownEvent, MouseUpEventMousePressedState
MouseEnterExitOnMouseEnter/ExitMouseEnterEvent, MouseExitEventMouseHoverState
MouseUpAsButtonOnMouseUpAsButtonMouseUpAsButtonEvent-

Animation (requires com.unity.modules.animation):

ProviderUnity CallbackEventsState component
AnimationEventAnimation Event (clip)AnimationEventEcsEvent-
StateMachineBehaviourOnStateEnter/ExitAnimatorStateEnterEvent, AnimatorStateExitEvent-

AnimationEventProvider — place on the same GameObject as the Animator. In the animation clip, set the event function name to OnAnimationEvent. The event carries stringParameter, intParameter, floatParameter, objectReferenceParameter and animationState.

StaticEcsStateMachineBehaviour — add to Animator Controller states. Not a MonoBehaviour — inherits from StateMachineBehaviour. Entity variant (StaticEcsEntityStateMachineBehaviour) stores a serialized EntityGID field.

StaticEcsStateMachineBehaviourLinker — a MonoBehaviour placed on the same GameObject as the Animator. Stores a reference to StaticEcsEntityProvider and on Start() automatically calls SetEntityGID() on all StaticEcsEntityStateMachineBehaviour found in the Animator Controller. Has a public Link() method for manual re-linking (e.g. after changing entities at runtime).

For entity modes, events have the Entity suffix: ClickEntityEvent, CollisionEnter3DEntityEvent, MouseDownEntityEvent, etc.
These events contain an additional EntityGID field

State components

State components are automatically managed by entity providers (EntityGID/EntityRef):

  • Added to the entity on Enter event
  • Removed from the entity on Exit event
  • Updated on Move/Update event (e.g. DragState)
public struct Collision3DState : IComponent {
    public Collider Collider;
    public Vector3 Point;
    public Vector3 Normal;
    public Vector3 Velocity;
}

public struct DragState : IComponent {
    public Vector2 Position;
    public int PointerId;
    public Vector2 Delta;
    public PointerEventData.InputButton Button;
}

public struct PointerHoverState : IComponent { }
public struct PointerPressedState : IComponent { }

Validation

All providers check CanSend() before sending:

  • Base check: world is initialized (World<TWorld>.Status == WorldStatus.Initialized)
  • GUI check: additionally checks that Selectable.interactable == true (if assigned)
  • Entity check: EntityGID.TryUnpack<TWorld>() verifies the entity is alive before setting/deleting components

Customization

All event sending and component management methods are virtual and can be overridden:

public sealed class MyCollision3D : Collision3DProvider<MyWorldType> {
    protected override bool CanSend() {
        return base.CanSend() && gameObject.activeInHierarchy;
    }

    protected override void OnSendEnterEvent(Collision data) {
        // Custom logic instead of or in addition to base
        base.OnSendEnterEvent(data);
    }
}

For entity providers, the following are also available:

protected override void OnAddComponent(Collision data) { ... }
protected override void OnRemoveComponent() { ... }

Setting EntityGID / EntityProvider from code

// For EntityGID providers
GetComponent<MyCollision3DEntityGID>().SetEntityGID(entityGid);

// For EntityRef providers
GetComponent<MyCollision3DEntityRef>().SetEntityProvider(entityProvider);

Usage example

using W = World<MyWorldType>;

// 1. Generate providers: Assets/Create/Static ECS/Providers
// 2. Add the required provider component to a GameObject
// 3. Register event receivers and handle events in a system:

public struct HandleClickSystem : IInitSystem, IUpdateSystem {
    private EventReceiver<MyWorldType, ClickEvent> _receiver;

    public void Init() {
        _receiver = W.RegisterEventReceiver<ClickEvent>();
    }

    public void Update() {
        foreach (var evt in _receiver) {
            ref var data = ref evt.Value;
            Debug.Log($"Click on {data.Ref.name} at {data.Position}");
        }
    }
}

// Or for entity events:
public struct HandleCollisionSystem : IInitSystem, IUpdateSystem {
    private EventReceiver<MyWorldType, CollisionEnter3DEntityEvent> _receiver;

    public void Init() {
        _receiver = W.RegisterEventReceiver<CollisionEnter3DEntityEvent>();
    }

    public void Update() {
        foreach (var evt in _receiver) {
            if (evt.Value.EntityGID.TryUnpack<MyWorldType>(out var entity)) {
                // Handle collision for a specific entity
            }
        }
    }
}

// Or use state components (EntityEventMode.ComponentOnly / EntityEventMode.All):
// Components are automatically added on enter and removed on exit
public struct HandleCollisionStateSystem : IUpdateSystem {
    public void Update() {
        foreach (var entity in W.Query<All<Collision3DState>>().Entities()) {
            ref var state = ref entity.Ref<Collision3DState>();
            Debug.Log($"Entity is colliding with {state.Collider.name}, velocity: {state.Velocity}");
        }
    }
}

Note: State components are fully compatible with change tracking. Components are added/removed from Unity callbacks (FixedUpdate), and tracking bits are written to the current tick. Systems in both FixedUpdate and Update will see these changes as long as W.Tick() is called at the end of Update. For example, you can use AllAdded<Collision3DState> to detect the moment a collision begins:

foreach (var entity in W.Query<All<Collision3DState>, AllAdded<Collision3DState>>().Entities()) {
}

Templates:

Class generators are available in the Assets/Create/Static ECS/ asset creation menu

Providers

Generator for sealed providers for a specific world
See Unity event providers section for details

Static ECS view window:

WindowMenu.png

Entities/Table - entity view table

EntitiesTable.png

  • Filter allows select the necessary entities
  • Entity GID allows to find an entity by its global identifier
  • Select allows to select the columns to be displayed
  • Show data allows to select the data to be displayed in the columns
  • Max entities result maximum number of displayed entities

To display component data in a table, you must: set the StaticEcsEditorTableValue attribute in the component for field or property

public struct Position : IComponent {
    [StaticEcsEditorTableValue]
    public Vector3 Val;
}

To display a different component name, you must set the StaticEcsEditorName attribute on the component

[StaticEcsEditorName("My velocity")]
public struct Velocity : IComponent {
    [StaticEcsEditorTableValue]
    public float Val;
}

To set the color of the component, you must set the StaticEcsEditorColor attribute in the component (you can set RGB or HEX color)

[StaticEcsEditorColor("f7796a")]
public struct Velocity : IComponent {
    [StaticEcsEditorTableValue]
    public float Val;
}

To group related components and tags into a collapsible section in the entity inspector, set the StaticEcsEditorGroup attribute. All components and tags sharing the same group name are rendered inside one foldout, sorted alphabetically by group name and placed before ungrouped components. The group name can optionally be paired with a color (RGB or HEX), shown as a colored vertical bar and bold header label. Each group's expanded/collapsed state is persisted per-world in the view config.

[StaticEcsEditorGroup("Movement", "00FF00")]
public struct Velocity : IComponent {
    public float Val;
}

[StaticEcsEditorGroup("Movement")]
public struct Frozen : ITag { }

entity control buttons are also available

  • eye icon - open the entity for viewing
  • lock - lock the entity in the table
  • trash - destroy the entity in the world

Viewer - entity view window

Displays all entity data with the ability to modify, add and delete components

EntitiesViewer.png

By default, only public object fields marked with the attribute [Serializable]

  • To display a private field, you must mark it with the attribute [StaticEcsEditorShow]
  • To hide a public field, you must mark it with the attribute [StaticEcsEditorHide]
  • To disable value editing in play mode, you can mark it with the attribute [StaticEcsEditorRuntimeReadOnly]
public struct SomeComponent : IComponent {
    [StaticEcsEditorShow]
    [StaticEcsEditorRuntimeReadOnly]
    private int _showData;
    
    [StaticEcsEditorHide]
    public int HideData;
}

Entities/Builder - entity constructor

Allows you to customize and create a new entity at runtime (Similar to entity provider)

EntitiesBuilder.png

Stats - statistics window

Displays all world, component and event data

Stats.png

Events/Table - event table

Displays recent events, their details and the number of subscribers who have not read the event

EventsTable.png

Events marked in yellow mean they are suppressed
Events marked in gray mean that they have been read by all subscribers

To display these components in a table, you must set the StaticEcsEditorTableValue attribute in the event for field or property

public struct DamageEvent : IEvent {
    public float Val;

    [StaticEcsEditorTableValue]
    public string ShowData => $"Damage {Val}";
}

The StaticEcsEditorColor attribute must be set to set the event color (can be set to RGB or HEX color)

[StaticEcsEditorColor("f7796a")]
public struct DamageEvent : IEvent {

}

The StaticEcsIgnoreEvent attribute must be set to ignore the event in the editor

[StaticEcsIgnoreEvent]
public struct DamageEvent : IEvent {

}

Viewer - event viewer

Allows you to view and modify (unread only) event data

EventsViewer.png

Events/Builder - event constructor

Allows you to configure and create a new event at runtime

EventsBuilder.png

Systems

Displays all systems in the order in which they are executed
Allows turning systems on and off during runtime
Displays the average execution time of each system

Systems.png

Settings

Allows you to configure the editor window behavior. Settings are stored in a StaticEcsViewConfig ScriptableObject asset that is automatically created on first use.

  • Config asset — reference to the active config. You can create multiple configs via Assets/Create/Static ECS/View Config and switch between them
  • Component Foldouts — controls automatic expansion of component foldouts on entities:
    • ExpandAll — all components are expanded by default
    • CollapseAll — all components are collapsed by default
    • Custom — only selected component types are auto-expanded
  • Reset config to defaults — resets all settings for the current world to defaults

Settings are persisted between sessions. The following state is saved:

  • Selected tab, draw rate
  • Entity table: visible columns, sort column, pinned entities, filters, max entity count
  • Event table: event type filters, auto-scroll mode
  • Stats: fragmentation threshold, show unregistered toggle
  • Systems: max nesting depth for system property display

Settings are auto-saved periodically (every 30 seconds) and on play mode exit.

Fix Broken Providers

Window: Tools > Static ECS > Fix Broken Providers

When a component, tag, event, link, multi or wrapper class is renamed, moved between assemblies or deleted, the corresponding SerializeReference slots inside Unity providers (in scenes and in prefabs) become "missing types". This window restores them in bulk.

Capabilities

  • Two scan modes:
    • Active Scene — scans only the currently active scene (other loaded scenes are skipped — a hint is shown when multi-scene editing is detected).
    • Prefabs Folder — scans every .prefab under a folder of your choice (prefab variants included). Defaults to Assets/.
  • Auto-fix all by GUID — for every group whose missing identity matches a known type GUID in StaticEcsTypeGuidRegistry, rewrites all affected slots to the current type. Renames and assembly moves are recovered automatically.
  • Replace group with… — manually pick the new type for a whole group. If the wrapper kind cannot be inferred (the wrapper class itself is missing), the dropdown lists every registered type across all kinds.
  • Auto-fix group / Remove group — per-group actions. Remove deletes the slots from providers array / eventTemplate and saves affected prefab assets via PrefabUtility.SavePrefabAsset.

How it works

The window calls SerializationUtility.GetManagedReferencesWithMissingTypes on each provider, groups missing entries by (class, namespace, assembly, kind), and rewrites the YAML of the affected scene / prefab files in place by referenceId. Successfully fixed entries disappear from the UI immediately — no manual rescan needed.

In-inspector single-slot fixing is also available on individual provider components: a broken slot shows a Replace… / Apply (auto-match) / Remove row directly in the entity provider editor.

Notes

  • Scanning runs only on window open and on Rescan button — there are no automatic subscriptions to scene/asset changes.
  • For best automatic recovery, mark all components/tags/events/links so they end up in the type GUID registry; then Auto-fix by GUID handles renames without manual work.
  • Duplicate detection is built in: the same physical broken slot reachable through multiple prefabs (nested prefabs, variants) is shown only once.

Questions

How to create a custom drawing method for a type?

To implement your own editor for a specific type, create a PropertyDrawer with the [CustomPropertyDrawer] attribute in the Editor folder of your project

Example:

[CustomPropertyDrawer(typeof(MyStruct))]
public class MyStructPropertyDrawer : PropertyDrawer {
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
        EditorGUI.BeginProperty(position, label, property);
        // Custom drawing logic
        EditorGUI.EndProperty();
    }
}

How to use attributes without having a dependency on this module?

It is necessary to copy the attributes by saving the namespace from \Runtime\Attributes.cs, after that the attributes will be correctly detected by the editor.

License

MIT license