Entity Component System framework aiming for syntax and usage simplicity with maximum performance for game development.
Entity Component System framework aiming for syntax and usage simplicity with maximum performance for game development.
DefaultEcs is an Entity Component System framework which aims to be accessible with little constraints while retaining as much performance as possible for game development.
DefaultEcs heavily uses features from C#7.0 and Span from the System.Memory package, compatible from .NETStandard 1.1.
For development, a C#9.0 compatible environment, net framework 4.8, net6.0 are required to build and run all tests (it is possible to disable some targets in the test project if needed).
It is possible to use DefaultEcs in Unity (check FAQ).
This is the current strategy used to version DefaultEcs: v0.major.minor
To help development with DefaultEcs, there is a roslyn analyzer which provides code generation and warnings against potential bad usage. It can be found here.
The World class acts as the central hub to create entities, query specific entities, get all components for a given type or publish and subscribe to messages that can be used to communicate across types.
Multiple World objects can be used in parallel, each instance being thread-safe from one another but operations performed on a single instance and all of its created items should be thought as non thread-safe. Depending on what is done, it is still possible to process operations concurrently to optimise performance.
Worlds are created as such:
World world = new World();The World class also implements the IDisposable interface so you can easily clean up resources by disposing it.
It is possible to send and receive message transiting in a World:
void On(in bool message) { }
// the method On will be called back every time a bool object is published
// it is possible to use any type
world.Subscribe(On);
world.Publish(true);It is also possible to subscribe to multiple methods of an instance by using the SubscribeAttribute:
public class Dummy
{
[Subscribe]
void On(in bool message) { }
[Subscribe]
void On(in int message) { }
void On(in string message) { }
}
Dummy dummy = new Dummy();
// this will subscribe the decorated methods only
world.Subscribe(dummy);
// the dummy bool method will be called
world.Publish(true);
// but not the string one as it dit not have the SubscribeAttribute
world.Publish(string.Empty);Note that the Subscribe method returns an IDisposable object acting as a subscription. To unsubscribe, simply dispose this object.
Entities are simple structs acting as keys to manage components.
Entities are created as such:
Entity entity = world.CreateEntity();You should not store entities yourself and rely as much as possible on the returned objects from a world query as those will be updated accordingly to component changes.
To clear an entity, simply call its Dispose method.
entity.Dispose();Once disposed, you should not use the entity again. If you need a safeguard, you can check the IsAlive property:
#if DEBUG
if (!entity.IsAlive)
{
// something is wrong
}
#endifYou can also make an entity act as if it was disposed so it is removed from world queries while keeping all its components, this is useful when you need to activate/deactivate an entity from your game logic:
entity.Disable();
// this will return false;
entity.IsEnabled();
entity.Enable();
// and now it will return true;
entity.IsEnabled();Components are not restricted by any heritage hierarchy. It is recommended that component objects only hold data and be structs to generate as little as possible garbage and to have them contiguous in memory, but you can use classes and interfaces too:
public struct Example
{
public float Value;
}
public interface IExample
{
float Value { get; set; }
}
public class CExample : IExample
{
public float Value { get; set; }
}To reduce memory usage, it is possible to set a maximum capacity for a given component type. If nothing is set, then the maximum entity count of the world will be used. This call needs to be done before any component of the specified type is set:
world.SetMaxCapacity(42);Components can live on two levels, on entities or directly on the world itself:
entity.Set(42);
// check if the entity has an int component
if (entity.Has())
{
// get the int component of the entity
if (--entity.Get() ();It is possible to share the same component value between entities or even the world. This is useful if you want to update a component value on multiple entities with a single call:
referenceEntity.Set(42);
entity.SetSameAs(referenceEntity);
referenceEntity.Set(1337);
// the value for entity will also be 1337
entity.Get();
world.Set("hello");
entity.SetSameAsWorld();
world.Set("world");
// the value for entity will also be "world"
entity.Get();If the component is removed from the entity used as reference or the world, it will not remove the component from the other entities using the same component.
Components on entities can also be disabled and re-enabled. This is useful if you want to quickly make an entity act as if its component composition has changed so it is picked up by world queries without paying the price of actually removing the component:
entity.Disable();
// this still return true
entity.Has();
// but this will return false
entity.IsEnabled();
entity.Enable();
// now this will return true
entity.IsEnabled();There are two ways to update a component value, either use the Set(newValue) method or set/edit the value returned by Get(). One major difference between the two is that Set(newValue) will also notify internal queries that the component value has changed:
entity.Set(42);
// we set a new value
entity.Set(1337);
ref int component = ref entity.Get();
// we have actually changed the component value but the internal queries have not been notified.
component = 42;
// we can notify manually by calling this method if we need to.
entity.NotifyChanged();By combining the previous calls, it is possible to define a component type as a singleton:
// only one component value for int can exist on this world
world.SetMaxCapacity(1);
// and it is used by the world
world.Set(42);
// entity.Set(10); this line would throw
// the only way to set the component on entities would be to use the world component
entity.SetSameAsWorld();Not all components can easily be serialized to be loaded from data files (texture, sound, ...). To help with the handling of those cases, helper types are provided to provide a way to load managed resources, shared across entities and even worlds, and automatically dispose them once no entity using them exists anymore.
To set up a managed resource on an entity, the type ManagedResource needs to be set as a component where TInfo is a type used as a single identifier for a single resource and information needed to load it, and TResource is the type of the resource.
Should multiple resources of the same type be needed on a single entity, it is also possible to set the type ManagedResource as a component.
If the ManagedResource component is removed from the entity or the entity holding it is disposed, the internal reference count on the resource will decrease and it will be disposed if zero is reached.
To actually load the resource, an implementation of the class AResourceManager is needed as shown in the example:
…This feature only cares for entity components, not the world components.
To perform operations, systems should query entities from the world. This is performed by requesting entities through the world and using the fluent API to create rules
…Once you are satisfied with the rules you can then finish the query by choosing how to get the entities:
world
.GetEntities()
...
.AsPredicate();Gets a Predicate that checks for your declared rules. When... rules are ignored.
This is useful if you need to check for some entity component composition.
world
.GetEntities()
...
.AsEnumerable();Gets an IEnumerable that when enumerated returns all the entities respecting your declared rules. When... rules are ignored.
This is useful if you need to do an initialisation on specific entities, this should not be used in a hot path.
world
.GetEntities()
...
.AsSet();Gets an EntitySet containing all entities respecting your declared rules. Its content is cached for fast access and is automatically updated as you change your entity component composition.
If When... rules are present, you should call its Complete() method once you are done processing its entities to clear it.
This is the base type used in the provided system implementation.
world
.GetEntities()
...
.AsMap();Gets an EntityMap which maps a single entity with a component type of TKey value. Its content is cached for fast access and is automatically updated as you change your entity component composition.
If When... rules are present, you should call its Complete() method once you are done processing its entities to clear it.
This is useful if you need O(1) access to an entity based on a key.
world
.GetEntities()
...
.AsMultiMap();Gets an EntityMultiMap which maps multiple entities with a component type of TKey value. Its content is cached for fast access and is automatically updated as you change your entity component composition.
If When... rules are present, you should call its Complete() method once you are done processing its entities to clear it.
This is useful if you need O(1) access to entities based on a key.
Although there is no obligation to use them, a set of base classes is provided to help with the creation of systems:
This is a base interface for all systems. It exposes an Update method and an IsEnabled property. In all derived t
No open issues yet, or sync has not completed.