Giter Club home page Giter Club logo

nurumi's Introduction

NUrumi

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.

Content

Core concepts

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.

Basics

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

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

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 ...
}

โš ๏ธ One important thing to known is that collector just collects entities which was touched but collector does not check actual state of entity. So if you track entities added to some group but other part of code removes entity from this group entity will present in collector until it will be clear.

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));

Relationships

NUrumi provides a Relationships. This mechanism provides two major possibilities:

  • allows to assign component multiple times
  • allows to build hierarchies

Assign component multiple times

// 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 []

Hierarchies

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();

Fields

NUrumi provides a wide range of predefined fields for different purposes.

Field

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

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

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)

PrimaryKey

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

IndexField

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);

nurumi's People

Contributors

volkovku avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.