This repository has been archived on 2023-02-02. You can view files and clone it, but cannot push or open issues or pull requests.
mfgames-nitride-cil/src/Nitride/Entities/README.md
2022-06-06 09:33:03 -05:00

6.5 KiB

Entities

Nitride is based on an Entity Component System (ECS) in the way it handles the various input documents, images, feeds, and other parts that make up a website. Implementing it this way makes it easier to create a distinction between the different entities (much like Statiq.Web uses the DocumentType) but allows for adding new components along the way without having the C# limitations of a sealed enumeration or needing to implement a Javascript-style enum for identification. Instead, if an entity needs to be identified as being Markdown, an image, or a database query, it just adds a component to represent that information.

The basic entity is just a simple object with an internal identifier. These entities are also immutable. Functions that appear to manipulate actually clone, make the change, and then return the results.

var entity = new Entity();
Console.WriteLine("Entity Id: {0}", entity.Id);

Components

By itself, an entity doesn't have any meaning or purpose. These are described by a generic collection of components that are added to the entity. Each component has a type and then an instance of that type. These are added to the entity with the Add command. If a type is not given, it is assumed to be the same type as the parameter, but a base class or interface can be given to allow different types to be stored in a specific component.

In effect, the type of the component is the key. Having two different types, even with the same object, would be considered two distinct objects.

string mimeType = "text/plain";
Entity entity = new Entity();
Assert.Equal(0, entity.Count);

Entity newEntity = entity.Add(mimeType);
Assert.Equal(0, entity.Count);
Assert.Equal(1, newEntity.Count);
Assert.Equal(entity.Id, newEntity.Id);
Assert.Equal(entity, newEntity);

newEntity = newEntity.Add<object>(mimeType);
Assert.Equal(2, newEntity.Count);

The basic operations for entity components are:

  • Add<TType>(component): Adds a component as the given type. If there is already a component there, an exception will be thrown.
  • Add(component): As Add<TType>(component) but the TType is the same as component.GetType().
  • Remove<TType>(): Removes any component of the given type, if exists. If there is no such component, then nothing happens.
  • Remove(component): Same as Remove<TType> with the component given determining the TType.
  • Set<TType>(component): Adds or updates a component of the given type.
  • Set(component): Same as Set<TType> with the component given determining the TType.
  • Copy(): Creates a copy of the entity and assigns it a new identifier.
  • ExactCopy(): Creates a copy of the entity with the same identifier.

As above, all of these return a new entity (or the same one if no change is made to the entity).

Query Components

  • bool Has<TType>(): Returns a value indicating whether the entity has the given component type.
  • TType Get<TType>(): Returns the value of the registered component. If there is no such object, this will throw an exception.
  • TType? GetOptional<TType>(): Returns the value of the registered component. If there is no such object, this will return the default value.
  • bool TryGet<TType>(out TType component): Attempt to get the component. If it cannot be retrieved, then this will return false and component is undefined.

Collections

To keep with the patterns of C#, working with collection of entities uses normal LINQ operations. For example, to combine two sets of entities together, the Union LINQ command can be used:

IEnumerable<Entity> entities1;
IEnumerable<Entity> entities2;
IEnumerable<Entity> all = entities1.Union(entities2);

To work with the ECS, additional extension methods have been written that allow for filtering or working with those entities.

HasComponents

The HasComponents is a set of overrides that checks to see if the given entity has the requisite components. If they don't, then that entity is filtered out.

IEnumerable<Entity> entities;

var filtered1 = entities.HasComponents<C1>();
var filtered2 = entities.HasComponents<C1, C2>();
var filtered3 = entities.HasComponents<C1, C2, C3>();

NotComponents

NotComponents is effectively the reverse of HasComponents in that if the entity has the given components, they are filtered out. This also allows up to three different components.

This also allows the developer to ask for an entity that has two components but not have a different of two with:

IEnumerable<Entity> entities;
var filtered = entities
    .HasComponents<C1, C2>()
    .NotComonents<C3, C4>();

ForComponents

ForComponents allows for a lambda to be performed on entities that have the given components while passing all the entities on through the function. This is much like the ForEach combined with Select in that the changed or updated entity will be passed on.

var entities = new Entities[]
{
    new Entity().Add("value1"),
    new Entity().Add(2),
    new Entity().Add(3).Add("value2"),
};
var filtered = entities
    .ForComponents<string>((entity, value) => entity.Set(value + "!"));

Assert.Equal(
    new[] {
        "value1!",
        null,
        "value2!",
    },
    filtered.Select(x => x.GetOptional<string>()));

There are also three overloads allowing up to three components to be pulled out with the lambda.

SetComponents, AddComponents, RemoveComponents

SetComponents (as the corresponding AddComponents, and RemoveComponents) basically perform the same operation on the entire list. They also have the three overloads to allow one to three components be manipulated in a single call.

IEnumerable<Entity> entities;
var updated = entities
    .AddComponents<object>(mimeType)
    .AddComponents(mimeType)
    .RemoveComponents<object>()
    .SetComponents<object>(mimeType)
    .SetComponent(mimeType);

MergeEntities

MergeComponents combines multiple entities together if they have the same Id field. The two sides of the comparison are the presence of a specific component.

IEnumerable<Entity> entities;
var combined = entities
            .MergeEntities<C1, C2>(
                (entity1, c1, entity2, c2) => entity1.Set(c2));

Files, Paths, and Content

Entities do not have an integral concept of being a file or having contents from the disk or anywhere else. Much of this is implemented as components from the Nitride.IO assembly which uses Zio for the underlying library, but can be easily replaced with a different IO layer (or even the straight System.IO).