CoreECS 1.4.4
See the version list below for details.
dotnet add package CoreECS --version 1.4.4
NuGet\Install-Package CoreECS -Version 1.4.4
<PackageReference Include="CoreECS" Version="1.4.4" />
<PackageVersion Include="CoreECS" Version="1.4.4" />
<PackageReference Include="CoreECS" />
paket add CoreECS --version 1.4.4
#r "nuget: CoreECS, 1.4.4"
#:package CoreECS@1.4.4
#addin nuget:?package=CoreECS&version=1.4.4
#tool nuget:?package=CoreECS&version=1.4.4
CoreECS - State First ECS Toolkit
CoreECS is a lightweight, easy-to-use Entity-Component-System (ECS) implementation designed for C#-based game applications. By combining ComponentStore and EntityGraph, it strikes an effective balance between flexibility and performance.
Unlike a full application framework, CoreECS is built for seamless integration. It can coexist alongside other ECS solutions, such as UnityECS, allowing you to incorporate it into existing projects with minimal friction.
The toolkit's design was refined through a turn-based card game which makes CoreECS inherently focused on efficient and robust state management for state-driven scenarios.
Why you should to use toolkit instead of framework?
Throughout my career, I have worked on card-based games as well as RPGs; on commercial multiplayer titles as well as low-budget indie projects. In these experiences, I have observed a fascinating phenomenon: we often see common design approaches appearing in games of different scales and genres—such as the design philosophy embodied by CoreECS. If we try to force complex problems into a single framework, we may lose the flexibility to handle issues creatively—your thinking often ends up revolving around engineering concerns, like whether your framework supports a certain feature, how to implement it, and how to balance development time with efficiency. Even game engines inherently carry a design philosophy that you must adapt to, and this constraint can become a major time-consuming challenge in software engineering.
That is why I believe a toolkit—a loosely coupled organizational approach—can more easily help developers bypass cumbersome engineering structures, select the right tools as needed, reduce development costs, and build software designs suited to the nature of their projects. This was my original motivation for developing CoreECS, and it also addressed a specific technical challenge I faced. My project initially planned to use Unity ECS for world updates, but Unity ECS proved quite clumsy in handling state changes—it sacrifices almost all flexibility for the sake of performance. To achieve my development goals, I had to write additional features to compensate. Moreover, due to the strict constraints Unity ECS imposes on C#, I struggled to work smoothly in my preferred development style. I needed a solution that balanced performance and flexibility.
Therefore, I designed CoreECS with a "state-first" philosophy and embedded it into my game to work alongside Unity ECS, achieving exactly that balance. I hope this lightweight toolkit can also help you build a development environment that fits your project—whether as a simple ECS manager or as a guardian for frontend-backend consistency, if it fits your needs, then it's the right choice!
Key Concepts
Those are really common concepts in ECS, and you can find them in most ECS implementations.
- Entity: A unique identifier that groups components together
- Component: A data structure that holds properties/data (no logic)
- System: Contains the logic that operates on entities with specific component combinations
- World: Container that manages entities, components, and systems
- Matcher: Defines criteria for selecting entities based on their components
- Collector: Tracks entities that match specific criteria and efficiently updates when entities change
- Injector: Resolves dependencies and injects them into systems, components, and collectors
- Tick: A single iteration of the ECS framework, where systems are processed in a defined order.
- Mask: A bitwise flag that is used to filter entities.
Quick Start Guide
1. Creating a World
A World represents the container for all entities, components, and systems. It manages the lifecycle of the ECS framework.
using CoreECS;
var world = new World();
world.Startup(); // Initialize the world
Before calling Startup(), you should NOT do any following operations:
- Create entities
- Add components to entities
- Register systems
But you can do those operations before World call Startup():
- Configure Injector by
World.Injector - Inherit
Worldclass to add custom logic like additional ECS managers or do something while world is build, start, tick and shutdown.
Be aware that the world is not thread-safe. You should only access the world from the main thread.
When this world is no longer used, ensure that you call World.Shutdown() to terminate this world and release all resources.
2. Defining Components
Components are simple data structures that implement the IComponent<T> interface. They hold data but don't contain logic.
public struct PositionComponent : IComponent<PositionComponent>
{
public float X;
public float Y;
}
public struct VelocityComponent : IComponent<VelocityComponent>
{
public float X;
public float Y;
}
public struct HealthComponent : IComponent<HealthComponent>
{
public float Value;
}
Components can optionally implement lifecycle methods:
public struct LifecycleComponent : IComponent<LifecycleComponent>
{
public bool OnCreateCalled;
public bool OnDestroyCalled;
public void OnCreate(ulong entityId)
{
OnCreateCalled = true;
}
public void OnDestroy(ulong entityId)
{
OnDestroyCalled = true;
}
}
3. Creating Entities
Entities serve as unique identifiers that bundle components together. While the underlying representation of an entity is a ulong value, it is recommended to use the Entity struct for better functionality and type safety. Use World.GetEntity() to convert a raw ulong identifier into a fully-featured Entity struct.
// Create an entity
var entity = world.CreateEntity();
// Get a fully functional entity via World.GetEntity(ulong)
var anotherEntity = world.GetEntity(entityId);
You can also add a mask to an entity to create types of entity.
enum EntityType {
Actor = 1 << 1,
Terrian = 1 << 2
}
// Create an entity with mask
var entityWithMask = world.CreateEntity((ulong) EntityType.Actor);
4. Adding Components to Entities
Components can be added to entities to give them properties and data.
// Add components to an entity
var velocityRef = entity.CreateComponent<VelocityComponent>();
velocityRef.RW.X = 1;
velocityRef.RW.Y = 1;
// Alternative way to set component data
entity.CreateComponent<HealthComponent>().RW.Value = 100;
// Create component with initial value (recommended for setting initial data)
var positionRef = entity.CreateComponent(new PositionComponent { X = 10, Y = 20 });
// Not recommended: Direct assignment ignores component's `OnCreate` callback
var positionRef2 = entity.CreateComponent<PositionComponent>();
positionRef2.RW = new PositionComponent { X = 10, Y = 20 };
5. Accessing Components
Retrieve components from entities to read or modify their data. If you only need to read the component data, use RO property. If you need to modify the component data, use RW property.
// Get a component from an entity
var positionRef = entity.GetComponent<PositionComponent>();
Console.WriteLine($"Position: ({positionRef.RO.X}, {positionRef.RO.Y})");
// Check if entity has a specific component
bool hasHealth = entity.HasComponent<HealthComponent>();
Console.WriteLine($"Has Health: {hasHealth}");
// Get all components of an entity
var allComponents = entity.GetComponents();
Console.WriteLine($"Total components: {allComponents.Length}");
RO / RW Access Notes
ROis read-only access and should be preferred when you only need to inspect values.RWis writable access; modifying throughRWmarks the component as changed and can trigger collector revision tracking (ChangedOnRevision).- If you only want to read in hot paths, avoid accidental writes through
RWto prevent unnecessary change events.
Helper Extension Methods
Entity provides convenient extension methods for common component operations:
// TryGetComponent - Safely get a component if it exists
if (entity.TryGetComponent<PositionComponent>(out var positionRef))
{
Console.WriteLine($"Position: ({positionRef.RO.X}, {positionRef.RO.Y})");
}
// GetOrCreateComponent - Get existing component or create new one
// Returns true if component existed, false if it was created
bool existed = entity.GetOrCreateComponent<VelocityComponent>(out var velocityRef);
if (!existed)
{
velocityRef.RW = new VelocityComponent { X = 1, Y = 1 };
}
// GetOrCreateComponent with initial value
entity.GetOrCreateComponent(out var healthRef, new HealthComponent { Value = 100 });
6. Removing Components
Components can be removed from entities when no longer needed.
// Remove a component by reference
entity.DestroyComponent(positionRef);
// Remove a component by type
entity.DestroyComponent<HealthComponent>();
7. Defining Systems
Systems contain the logic that operates on entities with specific component combinations. If you want a system to access world, managers or anything can be get from DI container, just put them on constructor.
If you need to grouping system and wish those group can being ticked one by one, the best way is to add mask on system to filter which systems it should process. You can tick those system when you call World.Tick(ulong mask).
Create a collector to find relevant entities is the default way to access entities in a system. As you can see, use World.CreateCollector() to request collector when this system created and we can get those entities via IEntityCollector.Collected. Before iterating collected entities, call Flush() to publish buffered changes to the front buffers.
public class MovementSystem : ISystem
{
private World m_world;
private IEntityCollector m_movingEntities;
public ulong TickGroup => ulong.MaxValue;
public MovementSystem(World world)
{
m_world = world;
}
public void OnCreate()
{
// Initialize system - create collectors to find relevant entities
m_movingEntities = m_world.CreateCollector(
EntityMatcher.With.OfAll<PositionComponent>().OfAll<VelocityComponent>()
);
}
public void OnTick(ulong tickMask)
{
// Process all entities that match the collector's criteria
m_movingEntities.Flush();
for (var i = 0; i < m_movingEntities.Collected.Count; i++)
{
var entity = m_world.GetEntity(m_movingEntities.Collected[i]);
var position = entity.GetComponent<PositionComponent>();
var velocity = entity.GetComponent<VelocityComponent>();
// Update position based on velocity
position.RW.X += velocity.RW.X;
position.RW.Y += velocity.RW.Y;
}
}
public void OnDestroy()
{
// Clean up resources
m_movingEntities?.Dispose();
}
}
For further details, please refer to EntityCollector.
8. Managing Systems
Register and manage systems within the world. The execution order of system is matched with the order you register system (First In First Tick). You should not changing system registration between World.BeginTick() and World.EndTick(). If you do that so, system will be added, but world modification will defer until next World.BeginTick().
// Create a world instance
var world = new World();
world.Startup();
// Register a system with the world
world.RegisterSystem<MovementSystem>();
// Find a system by type
var movementSystem = world.FindSystem<MovementSystem>();
// Tick the world in a loop
while (true)
{
world.BeginTick();
world.Tick();
world.EndTick();
}
9. Using Entity Matchers
Matchers allow you to filter entities based on their component composition.
// Match entities that have ALL of the specified components
var positionOnlyMatcher = EntityMatcher.With.OfAll<PositionComponent>();
// Match entities that have AT LEAST ONE of the specified components
var positionOrVelocityMatcher = EntityMatcher.With
.OfAny<PositionComponent>()
.OfAny<VelocityComponent>();
// Match entities that have specific components BUT NOT others
var positionWithoutHealthMatcher = EntityMatcher.With
.OfAll<PositionComponent>()
.OfNone<HealthComponent>();
// Complex matcher - entities with Position AND Velocity but WITHOUT Health
var complexMatcher = EntityMatcher.With
.OfAll<PositionComponent>()
.OfAll<VelocityComponent>()
.OfNone<HealthComponent>();
// Match entities with specific mask
var maskingMatcher = EntityMatcher.WithMask((ulong) EntityType.Actor);
If you create a matcher without any modification like entity mask or component rule, it will defaults to match any entity.
10. Entity Collector - Advanced Filtering and Change Tracking
EntityCollector is a powerful feature that tracks entities matching specific criteria and efficiently updates when entities change. It's essential for creating state-first game logic.
Basic Collector Usage
// Create a collector that tracks all entities with PositionComponent
var positionCollector = world.CreateCollector(
EntityMatcher.With.OfAll<PositionComponent>()
);
// Process collected entities
positionCollector.Flush(); // Call Flush() to apply any pending changes
for (int i = 0; i < positionCollector.Collected.Count; i++)
{
var entity = world.GetEntity(positionCollector.Collected[i]);
// Process the entity
}
IEntityCollector.Change() is still available for backward compatibility but marked obsolete; prefer Flush() in new code.
Collector Flags
Collectors support different behaviors through flags. The default flag is EntityCollectorFlag.Default:
// None - entities are immediately added/removed from Collected
var immediateCollector = world.CreateCollector(
EntityMatcher.With.OfAll<PositionComponent>(),
EntityCollectorFlag.None
);
// LazyAdd - newly matching entities won't appear in Collected until Flush() is called
var lazyAddCollector = world.CreateCollector(
EntityMatcher.With.OfAll<PositionComponent>(),
EntityCollectorFlag.LazyAdd
);
// LazyRemove - entities won't be removed from Collected until Flush() is called
var lazyRemoveCollector = world.CreateCollector(
EntityMatcher.With.OfAll<PositionComponent>(),
EntityCollectorFlag.LazyRemove
);
// Default (used when no flag is specified):
// Lazy + ChangedOnRevision + ChangedOnMatching
var lazyCollector = world.CreateCollector(
EntityMatcher.With.OfAll<PositionComponent>()
// EntityCollectorFlag.Default is used by default
);
Change Tracking
Collectors track which entities have changed since the last Flush() call:
var collector = world.CreateCollector(
EntityMatcher.With.OfAll<PositionComponent>()
);
// Add some entities with components
var entity1 = world.CreateEntity();
entity1.CreateComponent<PositionComponent>();
// Call Flush() to process pending changes
collector.Flush();
// Access change tracking lists
var matchingEntities = collector.Matching; // New entities that started matching
var clashingEntities = collector.Clashing; // Entities that stopped matching
foreach (var entityId in matchingEntities)
{
Console.WriteLine($"New matching entity: {entityId}");
}
foreach (var entityId in clashingEntities)
{
Console.WriteLine($"Stopped matching entity: {entityId}");
}
Changed behavior depends on collector flags:
ChangedOnRevision: include entities whose component data changed (default includes this).ChangedOnMatching: include entities that newly enter the collector (default includes this).ChangedOnClashing: include entities that leave the collector (disabled by default, enable explicitly when needed).
With EntityCollectorFlag.Default, Changed includes revision changes and newly matching entities, but not clashing entities.
Best Practices for Using Collectors
Always call
Flush()before processing collected entities to ensure the collection is up-to-date with recent changes.Do NOT use foreach loops when iterating over
Collected- use indexed for loops instead to prevent issues with potential collection modifications during iteration:
// CORRECT - Use indexed for loop
for (int i = 0; i < collector.Collected.Count; i++)
{
var entity = world.GetEntity(collector.Collected[i]);
// Process entity
}
// INCORRECT - Avoid foreach loops
// foreach (var entityId in collector.Collected) // This can cause issues
// {
// var entity = world.GetEntity(entityId);
// // Process entity
// }
- Proper cleanup - Always dispose collectors when no longer needed:
// In system's OnDestroy method
public void OnDestroy()
{
m_collector?.Dispose();
}
- Choose appropriate flags based on your use case:
- Use
Lazyflag when you want to process all changes at once in a predictable manner - Use
Noneflag when you need immediate updates to the collection
- Use
11. Complete Example
Here's a complete example demonstrating the basic usage:
using System;
using CoreECS;
using CoreECS.Defines;
// Define components
public struct PositionComponent : IComponent<PositionComponent>
{
public float X;
public float Y;
}
public struct VelocityComponent : IComponent<VelocityComponent>
{
public float X;
public float Y;
}
// Define system
public class MovementSystem : ISystem
{
private World m_world;
private IEntityCollector m_movingEntities;
public MovementSystem(World world)
{
m_world = world;
}
public void OnCreate()
{
m_movingEntities = m_world.CreateCollector(
EntityMatcher.With.OfAll<PositionComponent>().OfAll<VelocityComponent>()
);
}
public void OnTick(ulong tickMask)
{
m_movingEntities.Flush(); // Apply pending changes
for (var i = 0; i < m_movingEntities.Collected.Count; i++)
{
var entity = m_world.GetEntity(m_movingEntities.Collected[i]);
var position = entity.GetComponent<PositionComponent>();
var velocity = entity.GetComponent<VelocityComponent>();
position.RW.X += velocity.RW.X * 0.016f; // Assuming ~60 FPS
position.RW.Y += velocity.RW.Y * 0.016f;
}
}
public void OnDestroy()
{
m_movingEntities?.Dispose();
}
}
// Usage
class Program
{
static void Main(string[] args)
{
// Create and start world
var world = new World();
world.Startup();
// Register system
world.RegisterSystem<MovementSystem>();
// Create an entity with position and velocity
var entity = world.CreateEntity();
entity.CreateComponent<PositionComponent>().RW = new PositionComponent { X = 0, Y = 0 };
entity.CreateComponent<VelocityComponent>().RW = new VelocityComponent { X = 10, Y = 5 };
// Run simulation loop
for (int i = 0; i < 100; i++)
{
world.BeginTick();
world.Tick();
world.EndTick();
var position = entity.GetComponent<PositionComponent>();
Console.WriteLine($"Frame {i}: Position = ({position.RW.X:F2}, {position.RW.Y:F2})");
}
// Cleanup
world.Shutdown();
}
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.