DefaultEcs
Entity Component System framework aiming for syntax and usage simplicity with maximum performance for game development.
Install / Use
/learn @Doraku/DefaultEcsREADME
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.
Requirements
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).
Versioning
This is the current strategy used to version DefaultEcs: v0.major.minor
- 0: DefaultEcs is still in heavy development and although a lot of care is given not to break the current API, it can still happen
- major: incremented when there is a breaking change (reset minor number)
- minor: incremented when there is a new feature or a bug fix
Analyzer
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.
Overview
World
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.
Message
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<bool>(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.
Entity
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
}
#endif
You 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();
Component
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<Example>(42);
Components can live on two levels, on entities or directly on the world itself:
entity.Set<int>(42);
// check if the entity has an int component
if (entity.Has<int>())
{
// get the int component of the entity
if (--entity.Get<int>() <= 0)
{
// remove the int component from the entity
entity.Remove<int>();
}
}
// all those methods are also available on the World type
world.Set<int>(42);
if (world.Has<int>() && --world.Get<int>() <= 0)
{
world.Remove<int>();
}
// be careful that the component type is specific to the method generic parameter type, not the component type
entity.Set<IExample>(new CExample());
// this will return false as the component type previously set is IExample, not CExample
entity.Has<CExample>();
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<int>(42);
entity.SetSameAs<int>(referenceEntity);
referenceEntity.Set<int>(1337);
// the value for entity will also be 1337
entity.Get<int>();
world.Set<string>("hello");
entity.SetSameAsWorld<string>();
world.Set<string>("world");
// the value for entity will also be "world"
entity.Get<string>();
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<int>();
// this still return true
entity.Has<int>();
// but this will return false
entity.IsEnabled<int>();
entity.Enable<int>();
// now this will return true
entity.IsEnabled<int>();
There are two ways to update a component value, either use the Set<T>(newValue) method or set/edit the value returned by Get<T>(). One major difference between the two is that Set<T>(newValue) will also notify internal queries that the component value has changed:
entity.Set<int>(42);
// we set a new value
entity.Set<int>(1337);
ref int component = ref entity.Get<int>();
// 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<int>();
Singleton
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<int>(1);
// and it is used by the world
world.Set<int>(42);
// entity.Set<int>(10); this line would throw
// the only way to set the component on entities would be to use the world component
entity.SetSameAsWorld<int>();
Resource
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 provid
