The [ECS](https://en.wikipedia.org/wiki/Entity_component_system) Framework aims to maximize usability, modularity, extensibility and performance of dynamic entity changes. Without code generation and dependencies. Inspired by [LeoEcs Lite](https://github.com/Leopotam/ecslite).
The framework can be installed as a Unity package by adding the Git URL [in the PackageManager](https://docs.unity3d.com/2023.2/Documentation/Manual/upm-ui-giturl.html) or manually adding it to `Packages/manifest.json`:
Represent the core logic defining entity behaviors. They are implemented as user-defined classes that implement at least one of the process interfaces. Key processes include:
```c#
class SomeSystem : IEcsPreInit, IEcsInit, IEcsRun, IEcsDestroy
{
// Called once during EcsPipeline.Init() and before IEcsInit.Init().
public void PreInit () { }
// Called once during EcsPipeline.Init() and after IEcsPreInit.PreInit().
public void Init () { }
// Called each time during EcsPipeline.Run().
public void Run () { }
// Called once during EcsPipeline.Destroy().
public void Destroy () { }
}
```
> For implementing additional processes, refer to the [Processes](#Processes) section.
</br>
# Framework Concepts
## Pipeline
Container and engine of systems. Responsible for setting up the system call queue, provides mechanisms for communication between systems, and dependency injection. Implemented as the `EcsPipeline` class.
### Building
Builder is responsible for building the pipeline. Systems are added to the Builder and at the end, the pipeline is built. Example:
Queues in the system can be segmented into layers. A layer defines a position in the queue for inserting systems. For example, if a system needs to be inserted at the end of the queue regardless of where it is added, you can add this system to the `EcsConsts.END_LAYER` layer.
``` c#
const string SOME_LAYER = nameof(SOME_LAYER);
EcsPipeline pipeline = EcsPipeline.New()
// ...
// Inserts a new layer before the end layer EcsConsts.END_LAYER
.Layers.Insert(EcsConsts.END_LAYER, SOME_LAYER)
// System SomeSystem will be added to the SOME_LAYER layer
.Add(New SomeSystem(), SOME_LAYER)
// ...
.BuildAndInit();
```
The built-in layers are arranged in the following order:
*`EcsConst.PRE_BEGIN_LAYER`
*`EcsConst.BEGIN_LAYER`
*`EcsConst.BASIC_LAYER` (Systems are added here if no layer is specified during addition)
Processes are queues of systems that implement a common interface, such as `IEcsRun`. Runners are used to start processes. Built-in processes are started automatically. It is possible to implement custom processes.
<details>
<summary>Built-in processes</summary>
*`IEcsPreInit`, `IEcsInit`, `IEcsRun`, `IEcsDestroy` - lifecycle processes of `EcsPipeline`.
*`IOnInitInjectionComplete` - Similar to the [Dependency Injection](#Dependency-Injection) process, but signals the completion of initialization injection.
To add a new process, create an interface inherited from `IEcsProcess` and create a runner for it. A runner is a class that implements the interface of the process to be run and inherits from `EcsRunner<TInterface>`. Example:
// Creating and deleting an entity as shown in the Entities section.
var e = _world.NewEntity();
_world.DelEntity(e);
```
> **NOTICE:** It's necessary to call EcsWorld.Destroy() on the world instance when it's no longer needed, otherwise it will remain in memory.
### World Configuration
To initialize the world with a required size upfront and reduce warm-up time, you can pass an `EcsWorldConfig` instance to the constructor.
``` c#
EcsWorldConfig config = new EcsWorldConfig(
// Pre-initializes the world capacity for 2000 entities.
entitiesCapacity: 2000,
// Pre-initializes the pools capacity for 2000 components.
poolComponentsCapacity: 2000);
_world = new EcsDefaultWorld(config);
```
## Pool
Stash of components, providing methods for adding, reading, editing, and removing components on entities. There are several types of pools designed for different purposes:
*`EcsPool` - universal pool, stores struct components implementing the `IEcsComponent` interface;
Used to filter entities by the presence or absence of components.
``` c#
// Creating a mask that checks if entities have components
// SomeCmp1 and SomeCmp2, but do not have component SomeCmp3.
EcsMask mask = EcsMask.New(_world)
// Inc - Condition for the presence of a component.
.Inc<SomeCmp1>()
.Inc<SomeCmp2>()
// Exc - Condition for the absence of a component.
.Exc<SomeCmp3>()
.Build();
```
<details>
<summary>Static Mask</summary>
`EcsMask` is tied to specific world instances, which need to be passed to `EcsMask.New(world)`, but there is also `EcsStaticMask`, which can be created without being tied to a world.
These are custom classes inherited from `EcsAspect` and used to interact with entities. Aspects are both a pool cache and a component mask for filtering entities. You can think of aspects as a description of what entities the system is working with.
Simplified syntax:
``` c#
using DCFApixels.DragonECS;
// ...
class Aspect : EcsAspect
{
// Caches the Pose pool and adds it to the inclusive constraint.
public EcsPool<Pose> poses = Inc;
// Caches the Velocity pool and adds it to the inclusive constraint.
public EcsPool<Velocity> velocities = Inc;
// Caches the FreezedTag pool and adds it to the exclusive constraint.
public EcsTagPool<FreezedTag> freezedTags = Exc;
// During queries, it checks for the presence of components
// in the inclusive constraint and absence in the exclusive constraint.
// There is also Opt - it only caches the pool without affecting the mask.
}
```
Explicit syntax (the result is identical to the example above):
If there are conflicting constraints between the combined aspects, the new constraints will replace those added earlier. Constraints from the root aspect always replace constraints from added aspects. Here's a visual example of constraint combination:
Filter entities and return collections of entities that matching conditions. The built-in `Where` query filters by component mask matching and has several overloads:
+ `EcsWorld.Where(EcsMask mask)` - Standard filtering by mask;
+ `EcsWorld.Where<TAspect>(out TAspect aspect)` - Combines filtering by aspect mask and aspect return;
The `Where` query can be applied to both `EcsWorld` and framework collections (in this sense, `Where` is somewhat similar to the one in Linq). There are also overloads for sorting entities using `Comparison<int>`.
> You can use an [Extension](#extensions) to simplify query syntax and interactions with components - [Simplified Syntax](https://github.com/DCFApixels/DragonECS-AutoInjections).
Collection of entities that is read-only and stack-allocated. It consists of a reference to an array, its length, and the world identifier. Similar to `ReadOnlySpan<int>`.
``` c#
// Where query returns entities as EcsSpan.
EcsSpan es = _world.Where(out Aspect a);
// Iteration is possible using foreach and for loops.
foreach (var e in es)
{
// ...
}
for (int i = 0; i <es.Count;i++)
{
int e = es[i];
// ...
}
```
> Although `EcsSpan` is just an array, it does not allow duplicate entities.
### EcsGroup
Sparse Set based auxiliary collection for storing a set of entities with O(1) add/delete/check operations, etc.
``` c#
// Getting a new group. EcsWorld contains pool of groups,
// so a new one will be created or a free one will be reused.
EcsGroup group = EcsGroup.New(_world);
// Release the group.
group.Dispose();
```
``` c#
// Add entityID to the group.
group.Add(entityID);
// Check if entityID exists in the group.
group.Has(entityID);
// Remove entityID from the group.
group.Remove(entityID);
```
``` c#
// WhereToGroup query returns entities as a read-only group EcsReadonlyGroup.
EcsReadonlyGroup group = _world.WhereToGroup(out Aspect a);
// Iteration is possible using foreach and for loops.
Groups are sets and implement the `ISet<int>` interface. The editing methods have two variants: one that writes the result to `groupA`, and another that returns a new group.
// Difference of all entities in world and groupA.
groupA.Inverse();
EcsGroup newGroup = EcsGroup.Inverse(groupA);
```
## ECS Root
This is a custom class that is the entry point for ECS. Its main purpose is to initialize, start systems on each engine Update and release resources when no longer needed.
### Example for Unity
``` c#
using DCFApixels.DragonECS;
using UnityEngine;
public class EcsRoot : MonoBehaviour
{
private EcsPipeline _pipeline;
private EcsDefaultWorld _world;
private void Start()
{
// Creating world for entities and components.
_world = new EcsDefaultWorld();
// Creating pipeline for systems.
_pipeline = EcsPipeline.New()
// Adding systems.
// .Add(new SomeSystem1())
// .Add(new SomeSystem2())
// .Add(new SomeSystem3())
// Injecting world into systems.
.Inject(_world)
// Other injections.
// .Inject(SomeData)
// Finalizing the pipeline construction.
.Build();
// Initialize the Pipeline and run IEcsPreInit.PreInit()
// and IEcsInit.Init() on all added systems.
_pipeline.Init();
}
private void Update()
{
// Invoking IEcsRun.Run() on all added systems.
_pipeline.Run();
}
private void OnDestroy()
{
// Invoking IEcsDestroy.Destroy() on all added systems.
_pipeline.Destroy();
_pipeline = null;
// Requires deleting worlds that will no longer be used.
_world.Destroy();
_world = null;
}
}
```
### Generic example
``` c#
using DCFApixels.DragonECS;
public class EcsRoot
{
private EcsPipeline _pipeline;
private EcsDefaultWorld _world;
// Engine initialization .
public void Init()
{
// Creating world for entities and components.
_world = new EcsDefaultWorld();
// Creating pipeline for systems.
_pipeline = EcsPipeline.New()
// Adding systems.
// .Add(new SomeSystem1())
// .Add(new SomeSystem2())
// .Add(new SomeSystem3())
// Внедрение мира в системы.
.Inject(_world)
// Other injections.
// .Inject(SomeData)
// Finalizing the pipeline construction.
.Build();
// Initialize the Pipeline and run IEcsPreInit.PreInit()
// and IEcsInit.Init() on all added systems.
_pipeline.Init();
}
// Engine update loop.
public void Update()
{
// Invoking IEcsRun.Run() on all added systems.
_pipeline.Run();
}
// Engine cleanup.
public void Destroy()
{
// Invoking IEcsDestroy.Destroy() on all added systems.
_pipeline.Destroy();
_pipeline = null;
// Requires deleting worlds that will no longer be used.
_world.Destroy();
_world = null;
}
}
```
</br>
# Debug
The framework provides additional tools for debugging and logging, independent of the environment. Also many types have their own DebuggerProxy for more informative display in IDE.
## Meta Attributes
By default, meta-attributes have no use, but are used in integrations with engines to specify display in debugging tools and editors. And can also be used to generate automatic documentation.
``` c#
using DCFApixels.DragonECS;
// Specifies custom name for the type, defaults to the type name.
> To automatically generate unique identifiers MetaID, there is the method `MetaID.GenerateNewUniqueID()` and the [Browser Generator](https://dcfapixels.github.io/DragonECS-MetaID_Generator_Online/).
Has a set of methods for debugging and logging. It is implemented as a static class calling methods of Debug services. Debug services are intermediaries between the debugging systems of the environment and EcsDebug. This allows projects to be ported to other engines without modifying the debug code, by implementing the corresponding Debug service.
By default, `DefaultDebugService` is used, which outputs logs to the console. To implement a custom one, create a class inherited from `DebugService` and implement abstract class members.
+ `DRAGONECS_DISABLE_POOLS_EVENTS` - Disables reactive behavior in pools.
+ `DRAGONECS_ENABLE_DEBUG_SERVICE` - Enables EcsDebug functionality in release builds.
+ `DRAGONECS_STABILITY_MODE` - By default, for optimization purposes, the framework skips many exception checks in the release build. This define, instead of disabling checks, replaces them with code that resolves errors. This increases stability but reduces execution speed.
+ `DRAGONECS_DISABLE_CATH_EXCEPTIONS` - Turns off the default exception handling behavior. By default, the framework will catch exceptions with the exception information output via EcsDebug and continue working.
Constructors of `EcsWorld` and `EcsPipeline` classes can accept config containers implementing `IConfigContainer` or `IConfigContainerWriter` interface. These containers can be used to pass data and dependencies. The built-in container implementation is `ConfigContainer`, but you can also use your own implementation.</br>
Components can be used to attach additional data to worlds. World components are `struct` types. Access to components via `Get` is optimized, the speed is almost the same as access to class fields.
* [Code Templates for IDE](https://gist.github.com/ctzcs/0ba948b0e53aa41fe1c87796a401660b) and [for Unity](https://gist.github.com/ctzcs/d4c7730cf6cd984fe6f9e0e3f108a0f1)