ECS engine with aiming to extensibility and flexibility like an Entitas and performance near the LeoEcsLite project.
NUrumi project is free from dependencies and code generation.
Like an other ECS engines NUrumi lays on Entities and Components. NUrumi doesn't provides systems because their implementation is trivial and varies from project to project.
Entity is a logic union of components. In NUrumi entity represented as integer value also known as identifier which can be associated with components.
All entities lives in context. Each context holds entities and their components. A components allowed in context specified in special class called registry.
Component is a part of entity. In NUrumi component is a composition of fields. Each component field in NUrumi has special type like Field, RefField, IndexField, ReactiveField. It allows to balance between performance and flexibility. For simple structs and primitive types you can use Field which provides best performance. But if you want to track field value changes you can use ReactiveField which of course has less performance as Field. As a developer you can choose what you need in any situation. Types of fields will be explained in next sections.
Lest see some code
// Registry of our game which containns a set of registered components
public class GameRegistry : Registry<GameRegistry>
{
public PositionComponent Position;
public VelocityComponent Velocity;
public PlayerNameComponent PlayerName;
}
// A component to hold entity position
public class PositionComponent : Component<PositionComponent>
{
public Field<Vector3> Value;
}
// A component to hold entity velocity
public class VelocityComponent : Component<VelocityComponent>
{
public Field<Vector3> Value;
}
// A component to hold player name
public class PlayerNameComponent : Component<PlayerNameComponent>
{
public RefField<string> Value;
}
// An example function
void Main()
{
// Create new game context
var context = new Context<GameRegistry>();
// Cache fields to reduce noise in code
var playerName = context.Registry.PlayerName.Value;
var position = context.Registry.Position.Value;
var velocity = context.Registry.Velocity.Value;
// Create new entity in context whith components
var thor = context
.CreateEntity()
.Set(playerName, "Thor")
.Set(position, Vector3.Zero)
.Set(velocity, Vector3.Forward);
// Print entity name
Console.WriteLine(thor.Get(playerName));
// Prints true because thor has a name
Console.WriteLine(thor.Has(context.Registry.PlayerName));
// For strucure type Field class provies performance efficient methods
ref var thorPosition = ref thor.GetRef(position);
ref var thorVelocity = ref thor.GetRef(velocity);
position.Value += thorVelocity;
// Removes velocity from entity
thor.Remove(velocity);
// Removes an entity with all it components
context.RemoveEntity(thor);
}
As you can see usage of NUrumi is easy but some code looks like a noise.
Components which contains only one field can be reduced to form
of Component<TComponent>.Of<TValue>
or Component<TComponent>.OfRef<TValue>
.
Our example can be rewrote to:
// Registry of our game which containns a set of registered components
public class GameRegistry : Registry<GameRegistry>
{
public PositionComponent Position;
public VelocityComponent Velocity;
public PlayerNameComponent PlayerName;
}
// A component to hold entity position
public class PositionComponent : Component<PositionComponent>.Of<Vector3> {}
// A component to hold entity velocity
public class VelocityComponent : Component<VelocityComponent>.Of<Vector3> {}
// A component to hold player name
public class PlayerNameComponent : Component<PlayerNameComponent>.OfRef<string> {}
// An example function
void Main()
{
// Create new game context
var context = new Context<GameRegistry>();
// Cache fields to reduce noise in code
var playerName = context.Registry.PlayerName;
var position = context.Registry.Position;
var velocity = context.Registry.Velocity;
// Create new entity in context whith components
var thor = context
.CreateEntity()
.Set(playerName, "Thor")
.Set(position, Vector3.Zero)
.Set(velocity, Vector3.Forward);
// Print entity name
Console.WriteLine(thor.Get(playerName));
// Prints true because thor has a name
Console.WriteLine(thor.Has(playerName));
// For strucure types Field class provies performance efficient methods
ref var thorPosition = ref thor.GetRef(position);
ref var thorVelocity = ref thor.GetRef(velocity);
// Mutate entity position
position.Value += thorVelocity;
// Removes velocity from entity
thor.Remove(velocity);
// Removes an entity with all it components
context.RemoveEntity(thor);
}
Groups provides a fast way to iterate over an entities with specific subset of components.
// Create a group which contains all entities in a context with
// Position and Velocity components
var group = context.CreateGroup(GroupFilter
.Include(position)
.Include(velocity));
// Iterate over all entities in the group
foreach (var entity in group)
{
ref var entityPosition = ref entity.GetRef(position);
ref var entityVelocity = ref entity.GetRef(velocity);
entityPosition.Value += entityVelocity;
}
Subset of components in group can be specified with GroupFilter
and
their methods Include(component)
and Exclude(component)
.
Groups in NUrumi contains cached entities. So you can iterate over group without addition costs.
In some cases you may want to known when entities added or removed from group.
You can achieve it via group event OnGroupChanged(int entityIndex, bool add)
.
// Subscribe on group changes
group.OnGroupChanged += (entity, add) =>
{
Console.WriteLine($"Entity #{entity} was {(add ? "added" : "removed")}");
}
One more ability of NUrumi groups is that you can mutate it entities in iteration progress. All mutation will be accumulated through iteration process and applied when iteration was finished. So you don't need to create some temp buffers to interact with you entities.
Collector provides an easy way to collect changes in a group or reactive fields over time.
public class GameRegistry : Registry<GameRegistry>
{
public HealthComponent Health;
public PositionComponent Position;
public VelocityComponent Velocity;
...
}
public class HealthComponent : Component<HealthComponent>
{
public ReactiveField<int> Value;
}
var context = new Context<TestRegistry>();
var position = context.Registry.Position;
var velocity = context.Registry.Velocity;
var health = context.Registry.Health.Value;
var group = context.CreateGroup(GroupFilter
.Include(position)
.Include(velocity));
var collector = context
.CreateCollector()
.WatchEntitiesAddedTo(group)
.WatchChangesOf(health);
// After this operation entity1 will be in collector
var entity1 = context
.CreateEntity()
.Set(position, /* some position */)
.Set(velocity, /* some velocity */);
// After this operation entity2 will be in collector
var entity2 = context.CreateEntity().Set(health, 100);
// Collected results can be dropped by Clear method
// after that you can collect changes of next iteration
collector.Clear();
You can iterate over collected entities. Mutations in loop will not break it.
foreach (var entity in collector)
{
// Do something ...
}
For example:
var group = context.CreateGroup(GroupFilter
.Include(position)
.Include(velocity));
var collector = context
.CreateCollector()
.WatchEntitiesAddedTo(group);
// This code adds entitity to group
entity
.Set(position, /* some position */)
.Set(velocity, /* some velocity */);
// This code prints true
Console.WriteLine(collector.Has(entity));
// This code removes entitity from group
entity.Remove(position);
// This code prints true
Console.WriteLine(collector.Has(entity));
// Clear collector
collector.Clear();
// This code prints false
Console.WriteLine(collector.Has(entity));
NUrumi provides a Relationships. This mechanism provides two major possibilities:
- allows to assign component multiple times
- allows to build hierarchies
// Define registry wich contains Likes relationship
public class RelationRegistry : Registry<RelationRegistry>
{
public Likes Likes;
}
// Define relationship
public class Likes : Relation<Likes>
{
}
// Common boilerplate code
var context = new Context<RelationRegistry>();
var likes = context.Registry.Likes;
// Define entities
var alice = context.CreateEntity();
var apples = context.CreateEntity();
var burgers = context.CreateEntity();
var sweets = context.CreateEntity();
// Assing relationships
alice.Add(likes, apples);
alice.Add(likes, burgers);
alice.Add(likes, sweets);
// Get relationship; returns [apples, burgers, sweets]
var aliceLikes = alice.Relationship(likes);
// Get reverse-relationship; returns [alice]
var whoLikeApples = apples.Target(likes);
// Check is relationship exists
alice.Has(likes, apples);
apples.Targets(likes, alice);
// Remove relationship
alice.Remove(likes, apples);
// When entity removed all relationships will removed automatically
context.RemoveEntity(alice);
var whoLikeSweets = sweets.Target(likes); // returns []
Relationships is an easy way to organize hierarchies. Lets see how we can create parent-child relationship.
public class GameRegistry : Registry<GameRegistry>
{
public ChildOf ChildOf;
}
public class ChildOf : Relation<ChildOf>
{
}
// Common boilerplate code
var context = new Context<RelationRegistry>();
var childOf = context.Registry.ChildOf;
// Create hierarchy
var parent = context.CreateEntity();
var child1 = context.CreateEntity().Add(childOf, parent);
var child2 = context.CreateEntity().Add(childOf, parent);
// Get all children of parent
var children = parent.Target(childOf); // [child1, child2]
// Get parent
var childParent = child1.Relationship(childOf).Single();
NUrumi provides a wide range of predefined fields for different purposes.
Field is a simplest and most performant way to store data. Field have only one restriction it can not store class, or structs which contains class. Field is a perfect candidate for store positions, velocity, rotations and etc.
Field provides common methods:
TValue Get(int entityId)
bool TryGet(int entityId, out TValue result)
void Set(int entityId, TValue value)
Also Field provides high performance methods:
ref TValue GetRef(int entityId)
ref TValue GetOrAdd(int entityId)
ref TValue GetOrSet(int entityId, TValue value)
Common scenario of Field usage:
// Boilerplate
var context = new Context<GameRegistry>();
var position = context.Registry.Position.Value;
var velocity = context.Registry.Velocity.Value;
var group = context.CreateGroup(GroupFilter
.Include(position)
.Include(velocity));
// Some system executes something like this
foreach (var entity in group)
{
ref var pos = ref entity.GetRef(position);
ref var vel = ref entity.GetRef(velocity);
pos.Value += vel;
}
RefField is a sister of Field. It allows to store reference type values. As a handicap her hasn't high performance methods like a Field.
RefField provides methods:
TValue Get(int entityId)
bool TryGet(int entityId, out TValue result)
void Set(int entityId, TValue value)
ReactiveField can hold pure structures and track value changes.
To track value changes your can subscribe on OnValueChanged event.
ReactiveField provides events:
OnValueChanged(
IComponent component, // A component which field was changed
IField field, // A flied which values was changed
int entityId, // An identifier of entity which value was changed
TValue? oldValue, // The old value
TValue newValue) // The new value
ReactiveField provides methods:
TValue Get(int entityId)
bool TryGet(int entityId, out TValue result)
void Set(int entityId, TValue value)
A PrimaryKey provides way to add unique constraints.
PrimaryKey provides methods:
TKey Get(int entityId)
bool TryGet(int entityId, out TKey result)
void Set(int entityId, TKey value)
int GetEntityByKey(TKey key)
bool TryGetEntityByKey(TKey key, out int entityId)
public class Registry : Registry<Registry>
{
public ExternalIdComponent ExternalIdIndex;
}
public class ExternalIdComponent : Component<ExternalIdComponent>.OfPrimaryKey<ExternalEntityId>
{
}
public readonly struct ExternalEntityId : IEquatable<ExternalEntityId>
{
public ExternalEntityId(int externalId)
{
ExternalId = externalId;
}
public readonly int ExternalId;
public override string ToString()
{
return ExternalId.ToString();
}
public bool Equals(ExternalEntityId other)
{
return ExternalId == other.ExternalId;
}
public override bool Equals(object obj)
{
return obj is ExternalEntityId other && Equals(other);
}
public override int GetHashCode()
{
return ExternalId;
}
public static bool operator ==(ExternalEntityId left, ExternalEntityId right)
{
return left.Equals(right);
}
public static bool operator !=(ExternalEntityId left, ExternalEntityId right)
{
return !left.Equals(right);
}
}
// Boilerplate
var context = new Context<Registry>();
var externalIdIndex = context.Registry.ExternalIdIndex;
// Usage
var externalId = new ExternalEntityId(1);
var entity = context
.CreateEntity()
.Set(externalIdIndex, externalId);
Console.WriteLine(entity.Has(externalIdIndex)); // True
Console.WriteLine(entity.TryGet(externalIdIndex, out var entityExternalId)); // True
Console.WriteLine(entity.Get(externalIdIndex)); // 1
Console.WriteLine(entityExternalId); // 1
// Primary key capabilities
Console.WriteLine(externalIdIndex.TryGetEntityByKey(externalId, out var foundEntity)); // True
Console.WriteLine(foundEntity == entity); // True
Console.WriteLine(externalIdIndex.GetEntityByKey(externalId) == entity); // True
An IndexField provides a one-to-many relationship. It's alternative way to organize some kind of hierarchies based on external identifiers or composite keys.
PrimaryKey provides methods:
TKey Get(int entityId)
bool TryGet(int entityId, out TKey result)
void Set(int entityId, TKey value)
EntitiesSet GetEntitiesAssociatedWith(TValue value)
class TestRegistry : Registry<TestRegistry>
{
public Parent Parent;
}
class Parent : Component<Parent>
{
public IndexField<int> Value;
}
// Boilerplate
var context = new Context<TestRegistry>();
var parentComponent = context.Registry.Parent;
var parent = parentComponent.Value;
// Create hierarhy
var parentEntity = context.CreateEntity();
var childEntity1 = context.CreateEntity();
childEntity1.Set(parent, parentEntity);
var childEntity2 = context.CreateEntity();
childEntity2.Set(parent, parentEntity);
var childEntity3 = context.CreateEntity();
childEntity3.Set(parent, parentEntity);
// Get parent children
var children = parent.GetEntitiesAssociatedWith(parentEntity);