com.elestrago.unity.entitas-redux
v4.1.3
Published
Entitas Redux is an fast, accessible, and feature-rich ECS framework for Unity. It leverages code generation and an extensible plugin framework to make life easier for developers.
Readme
JCMG Entitas Redux
Installing
Using the native Unity Package Manager introduced in 2017.2, you can add this library as a package by modifying your
manifest.json file found at /ProjectName/Packages/manifest.json to include it as a dependency. See the example below
on how to reference it.
Install via NPM
The package is available on the npmjs registry.
Add registry scope
{
"dependencies": {
"com.elestrago.unity.entitas-redux": "x.x.x"
},
"scopedRegistries": [
{
"name": "eLeSTRaGo",
"url": "https://registry.npmjs.org",
"scopes": [
"com.elestrago.unity"
]
}
]
}Entitas Redux
About
This version of Entitas Redux is a reworked version of EntitasRedux with a sole focus on Unity.
Requirements
- Min Unity Version: 2022.3
Entitas Redux ECS Usage Guideline
This guideline describes the standard usage of the Entitas Redux ECS framework in this project.
1. Context Creation
Contexts are defined using the ContextAttribute. Declare a partial class inheriting from ContextAttribute.
using JCMG.EntitasRedux;
public sealed partial class GameAttribute : ContextAttribute
{
}The generator will automatically create GameContext, GameEntity, and GameMatcher.
2. Component Creation
Components must implement IComponent, have one or more context attributes, and their name should end with Component.
- Simple Component:
[Game]
public struct PositionComponent : IComponent
{
public float x;
public float y;
}- Flag Component:
Components with no fields are treated as flags. The generator creates a
Has[FlagName]readonly property and aSet[FlagName](bool value)method to manage them.
[Game]
public struct MovableComponent : IComponent
{
}3. Entity Indexes
Entity Indexes allow you to efficiently retrieve entities based on component values.
- PrimaryEntityIndex: Used for unique values.
[Game]
public struct IdComponent : IComponent
{
[PrimaryEntityIndex]
public int value;
}- EntityIndex: Used for non-unique values.
[Game]
public struct NameComponent : IComponent
{
[EntityIndex]
public string value;
}4. Unique Components
For components that should only exist on a single entity at any time, use the [Unique] attribute.
[Game, Unique]
public struct PlayerComponent : IComponent
{
}This generates a property on the context for direct access:
// Get the unique player entity
var player = context.PlayerEntity;
// Check if it exists
if (context.HasPlayer) { /* ... */ }
// Set a new unique entity (the old one will lose the component)
context.PlayerEntity = newPlayerEntity;5. Entity Creation and Mutation
Use the generated context and entity API to create and mutate entities.
- Creating an Entity:
var context = Contexts.SharedInstance.Game;
var entity = context.CreateEntity();- Adding, Replacing, and Removing Components:
// Add or replace a component with values
entity.AddPosition(10f, 20f);
// Safely add a component
if (entity.TryAddPosition(10f, 20f)) { /* Added */ }
// Remove a component
entity.RemovePosition();
// Safely remove a component
if (entity.TryRemovePosition()) { /* Removed */ }- Working with Flag Components: Flag components are managed with a setter method.
// Check for a flag
bool isMovable = entity.HasMovable;
// Add a flag. Triggers groups only if the flag was not already present.
entity.SetMovable(true);
// Remove a flag. Triggers groups only if the flag was present.
entity.SetMovable(false);- Entity Lifecycle and Safety:
Check if an entity is still active in the context using
IsAlive.
if (entity.IsAlive) { /* Safe to mutate */ }- Retrieving Entities by Index:
// Primary
var entity = context.GetEntityWithId(42);
// Non-primary
var entities = context.GetEntitiesWithName("Player");6. Feature and System Creation
Systems implement specific lifecycle interfaces, and Features group them together.
- Execution Systems: Implement
IInitializeSystem,IUpdateSystem,IFixedUpdateSystem,ILateUpdateSystem,ICleanupSystem, orITearDownSystem. - Asynchronous Systems: Implement
IInitializeAsyncSystemfor async setup logic. - Reactive Systems: Implement
IReactiveSystemto react to component changes.
// IUpdateSystem for frame-rate dependent logic
public class MovementSystem : IUpdateSystem
{
private readonly IGroup<GameEntity> _group;
public MovementSystem(GameContext context)
{
_group = context.GetGroup(GameMatcher.AllOf(GameMatcher.Position, GameMatcher.Velocity));
}
public void Update()
{
var dt = UnityEngine.Time.deltaTime;
using var _ = _group.GetEntities(out var buffer);
for (var i = 0; i < buffer.Length; i++)
{
var e = buffer[i];
var pos = e.Position;
var vel = e.Velocity;
e.AddPosition(pos.x + vel.x * dt, pos.y + vel.y * dt);
}
}
}
// IReactiveSystem for reacting to data changes
public class DebugLogHealthSystem : ReactiveSystem<GameEntity>
{
public DebugLogHealthSystem(GameContext context) : base(context) { }
protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
{
// Trigger when HealthComponent is added or replaced
return context.CreateCollector(GameMatcher.Health);
}
protected override bool Filter(GameEntity entity)
{
// Only process entities that have health and are still alive
return entity.HasHealth && entity.IsAlive;
}
protected override void Execute(System.Collections.Generic.List<GameEntity> entities)
{
foreach (var e in entities)
{
UnityEngine.Debug.Log($"Entity {e.Id} health: {e.Health.value}");
}
}
}- Features:
Create a class inheriting from
Featureto group systems.
public class GameSystems : Feature
{
public GameSystems(Contexts contexts) : base("Game Systems")
{
// Initialization
Add(new DataLoadSystem()); // IInitializeAsyncSystem
Add(new InitializeLevelSystem(contexts.Game)); // IInitializeSystem
// Logic
Add(new MovementSystem(contexts.Game)); // IUpdateSystem
Add(new DebugLogHealthSystem(contexts.Game)); // IReactiveSystem
// Cleanup
Add(new CleanupSystem()); // ICleanupSystem
}
}7. Group API
The IGroup<TEntity> interface provides efficient ways to access and observe entities:
- Optimized Iteration: Use
GetEntities(out var buffer)which returns anArrayDisposableto minimize allocations. - Single Entity: Use
GetSingleEntity()when you expect only one entity to match the matcher (throws if more than one exists). - Events: Subscribe to
OnEntityAdded,OnEntityRemoved, orOnEntityUpdatedto react to group changes. - Count: Get the current number of entities in the group via
Count.
8. Integration in Unity
Initialize and update your systems in a MonoBehaviour. Use LateUpdate for Cleanup to ensure it runs after all update
logic.
public class GameController : MonoBehaviour
{
private Systems _systems;
private CancellationTokenSource _cts;
async void Start()
{
_cts = new CancellationTokenSource();
var contexts = Contexts.SharedInstance;
_systems = new GameSystems(contexts);
// Run async initialization
await _systems.InitializeAsync(_cts.Token);
// Run regular initialization
_systems.Initialize();
}
void Update()
{
_systems.Update();
}
void FixedUpdate()
{
_systems.FixedUpdate();
}
void LateUpdate()
{
_systems.Cleanup();
}
void OnDestroy()
{
_cts?.Cancel();
_systems.TearDown();
}
}9. Data Transfer API
The Data Transfer API allows you to easily copy data between entities and POCO data objects. This is useful for serialization, networking, or saving/loading game state.
To enable this feature, add the [GenerateDataTransfer] attribute to your component.
[Game]
[GenerateDataTransfer]
public struct PlayerDataComponent : IComponent
{
public string Name;
public int Level;
public List<string> Inventory;
}This generates an interface IGamePlayerDataData and a static class GamePlayerDataDataTransfer with the following
methods. The interface properties follow the naming convention {ContextName}{ComponentName}{MemberName}.
- CopyTo: Copies data from an entity's component to a data object.
public class MyPlayerData : IGamePlayerDataData
{
public string GamePlayerDataName { get; set; }
public int GamePlayerDataLevel { get; set; }
public List<string> GamePlayerDataInventory { get; set; }
}
// ...
var data = new MyPlayerData();
GamePlayerDataDataTransfer.CopyTo(entity, data);- WriteTo: Copies data from a data object to an entity's component.
var data = new MyPlayerData { GamePlayerDataName = "Hero", GamePlayerDataLevel = 5 };
GamePlayerDataDataTransfer.WriteTo(entity, data);- Clear: Clears the data object (sets fields to default or clears collections).
GamePlayerDataDataTransfer.Clear(data);Nullable Support:
You can enable nullable support by setting Nullable = true in the attribute.
[Game]
[GenerateDataTransfer(Nullable = true)]
public struct OptionalDataComponent : IComponent
{
public int Value;
public List<int> Items;
}- Behavior with Nullable:
- WriteTo:
- If all fields in the data object are
null(e.g.GameOptionalDataValueis null), the component is **removed ** from the entity. - If any field is
null, it is either skipped (if value type) or set tonull(if reference type) on the component.
- If all fields in the data object are
- CopyTo:
- If a component field is
null(for collections), the data object's field is set tonull.
- If a component field is
- Add:
- When adding a new component,
nullvalues in the data object result indefaultvalues ornull(for collections) in the component.
- When adding a new component,
- WriteTo:
Skip Empty Support:
You can enable skip empty support by setting SkipEmpty = true in the attribute.
[Game]
[GenerateDataTransfer(SkipEmpty = true)]
public struct OptionalDataComponent : IComponent
{
public int Value;
public List<int> Items;
}- Behavior with SkipEmpty:
- WriteTo:
- If all fields in the data object are
default(e.g.0for int,nullor empty for collections), the component is not added to the entity.
- If all fields in the data object are
- WriteTo:
Ignore Data Transfer:
You can exclude specific members from data transfer using the [IgnorDataTransfer] attribute.
[Game]
[GenerateDataTransfer]
public struct PlayerDataComponent : IComponent
{
public string Name;
[IgnorDataTransfer]
public int RuntimeId; // This field will be ignored by the Data Transfer API
}Hooks:
You can execute custom logic before or after data is written to the component using [BeforeDataTransfer] and
[AfterDataTransfer] attributes on methods within the component.
[Game]
[GenerateDataTransfer]
public struct PlayerDataComponent : IComponent
{
public string Name;
[BeforeDataTransfer]
public void OnBeforeTransfer()
{
// Logic to execute before data is copied from the component (CopyTo)
}
[AfterDataTransfer]
public void OnAfterTransfer()
{
// Logic to execute after data is written to the component (WriteTo)
}
}10. Event System
You can generate event systems and API to react to component changes by adding the [Event] attribute to a component.
[Game, Event(EventTarget.Self)]
public struct PositionComponent : IComponent
{
public float x;
public float y;
}This generates methods on the entity to subscribe to changes.
Subscribe: The
Subscribemethod adds a listener and returns anIDisposablethat removes the listener when disposed. This is the recommended way to handle events.You can also control whether the delegate is invoked immediately upon subscription using the
invokeOnSubscribeparameter (default:truefor Added/Updated,falsefor Removed).
public class PositionController : MonoBehaviour
{
private System.IDisposable _listener;
void Start()
{
var entity = Contexts.SharedInstance.Game.CreateEntity();
// Subscribe to the Position event.
// By default, this will invoke OnPosition immediately with the current component values.
_listener = entity.SubscribePosition(OnPosition);
}
void OnPosition(GameEntity entity, float x, float y)
{
transform.position = new Vector3(x, y);
}
void OnDestroy()
{
// Unsubscribe by disposing the listener
_listener?.Dispose();
}
}- Add/Remove Listener:
You can also manually add and remove listeners using
Add[EventName]ListenerandRemove[EventName]Listener.
entity.AddPositionListener(OnPosition);
entity.RemovePositionListener(OnPosition);11. Common Pitfalls and Fixes
- Modifying Components During Iteration: Wrong: Modifying a component reference directly doesn't notify observers.
using var _ = _group.GetEntities(out var buffer);
for (var i = 0; i < buffer.Length; i++)
{
var e = buffer[i];
e.Position.x += 1; // Observers NOT notified
}Fix: Use AddXyz. AddXyz will replace the component if it already exists.
using var _ = _group.GetEntities(out var buffer);
for (var i = 0; i < buffer.Length; i++)
{
var e = buffer[i];
var pos = e.Position;
e.AddPosition(pos.x + 1, pos.y);
}Missing Cleanup/TearDown: Ensure
_systems.Cleanup()is called (usually inLateUpdate) and_systems.TearDown()is called (inOnDestroy) to process entity destruction, release memory, and prevent leaks.Reactive System Filtering: Always implement a strict
FilterinReactiveSystem(as shown in the example in Section 6) to ensure entities still match the required state whenExecuteis called.Event Listener Cleanup: Always remove listeners in
OnDestroyto prevent memory leaks. The easiest way is to use theSubscribemethod and dispose the returnedIDisposable.
void OnDestroy()
{
_listener?.Dispose();
}If using manual Add/Remove, ensure you remove the delegate.
void OnDestroy()
{
if (_entity != null && _entity.IsAlive)
{
_entity.RemovePositionListener(OnPosition);
}
}