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).
> It is also recommended to install the Unity engine integration extension [Unity integration](https://github.com/DCFApixels/DragonECS-Unity)
* ### Unity package
The framework supports installation as a Unity package by adding the Git URL to the PackageManager ([how-to](https://docs.unity3d.com/2023.2/Documentation/Manual/upm-ui-giturl.html)) or by manually adding the entry to `Packages/manifest.json`:
* [Code Templates for IDE](https://gist.github.com/ctzcs/0ba948b0e53aa41fe1c87796a401660b) and [for Unity](https://gist.github.com/ctzcs/d4c7730cf6cd984fe6f9e0e3f108a0f1)
> *Your extension? If you are developing an extension for DragonECS, you can share it [here](#feedback).
Container for components. There are two identifier types used to reference entities:
*`int` - short-lived identifier, valid within a single tick. Not recommended for long-term storage;
*`entlong` - long-term identifier that includes a generation tag, which makes it unique across entity lifetimes. Suitable for storing and long-term usage.
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:
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. Usually masks are not used standalone, they are part of `EcsAspect` and used by queries to filter entities.
// 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.
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.
The need to enable or disable systems usually appears when the overall game state changes; this may also mean switching a group of systems. Conceptually this is a change of processes. There are two solutions:
- If the process changes are global, create a new `EcsPipeline` and run the appropriate pipeline in the engine update loop.
- Split `IEcsRun` into multiple processes and run the desired process in the engine update loop. To do this create a new process interface, implement a runner for it, and obtain the runner via `EcsPipeline.GetRunner<T>()`.