Skip to content
Magicolo edited this page Feb 27, 2019 · 40 revisions

Entia

Entia is a free, open-source, data-oriented, highly performant, parallelizable and extensible Entity-Component-System (ECS) framework written in C# especially for game development. It takes advantage of the latest C#7+ features to represent state exclusively with contiguous structs. No indirection, no boxing, no garbage collection and no cache misses.

Since Entia is built using .Net Standard 2.0, it is compatible with .Net Core 2.0+, .Net Framework 4.6+, Mono 5.4+, Xamarin and any other implementation of .Net that follows the standard (see this page for more details). Therefore it is compatible with any game engine that has proper C# support.

For the full Unity game engine integration of the framework, see Entia.Unity.


Content


Installation

  • Download the most recent stable version of Entia.
  • Extract the 'Entia.zip' package in a relevant directory.
  • Add 'Entia.dll' and 'Entia.Core.dll' as dependencies in your project.
  • Optionally install the packaged Visual Studio 2017 extension 'Entia.Analyze.vsix' to get Entia specific code analysis.

Tutorial

  • Here is a snippet of code to get you up and running:
using Entia;
using Entia.Core;
using Entia.Modules;
using Entia.Phases;
using Entia.Systems;
using static Entia.Nodes.Node;

public static class Game
{
    public static void Run()
    {
        var world = new World();
        var controllers = world.Controllers();
        var resources = world.Resources();

        // As the name suggest, this execution node will execute its children 
        // in sequential order.
        var node = Sequence(
            // Insert systems here using 'System<T>()' where 'T' is your system type.
            System<Systems.Motion>(),
            System<Systems.ControllerInput>(),
            System<Systems.Health>());

        // A controller is built from the node to allow the execution of your systems.
        if (controllers.Control(node).TryValue(out var controller))
        {
            // This resource will allow the application to close.
            ref var game = ref resources.Get<Resources.Game>();
            // Executes a typical execution routine.
            controller.Run(in game.Quit);
        }
    }
}

namespace Resources
{
    public struct Game : IResource { public bool Quit; }
}

namespace Systems
{
    public struct Motion : IRun
    {
        public void Run() { /* TODO */ }
    }

    public struct ControllerInput : IRun
    {
        public void Run() { /* TODO */ }
    }

    public struct Health : IRun
    {
        public void Run() { /* TODO */ }
    }
}
  • For more details, please consult the wiki.

The Basics

ECS stands for Entity, Component, System. I will go into details about what each of those are, but they are going to be the elements that allow you to program efficiently and pleasantly in a data-oriented style. But what does 'data-oriented' mean, you ask? Well, data-oriented design (DOD) is a way of solving programming problems just like object-oriented programming (OOP). It differs mainly by its focus on memory layout which has some important ramifications such as separating data from logic. This separation makes programs more performant and more composable. Without going into details about DOD since many more knowledgeable articles already exist on the subject, it is to be known that Entia puts DOD at its core and that will translate in certain practices that require a certain amount of getting used to.

Ok, back to ECS. Most programmers have heard at some point that it is better to use composition over inheritance. That is because inheritance, even when well designed, is more rigid and harder to change after the fact compared to its composed equivalent and in game development, things tend to change all the time in unexpected ways. ECS takes the idea of composition to the extreme by removing inheritance and, as mentioned above, by separating data and logic. This effectively flattens the structure of programs and makes them much more granular and easier to assemble. So here's a brief definition of the essential elements of ECS:

  • Entity: an unique identifier (often implemented as a simple integer).
  • Component: a chunk of data associated with an Entity.
  • System: a piece of logic that processes a subset of all Entities and their associated Components.

So an Entity can be conceptually thought of as a container for Components, but this could be misleading since an Entity does not contain anything as it is only an identifier. All it does is group Components together such that they can be queried by Systems and as such it relates more to a key in a database table where the columns would be the Components than it does to a bag of Components.

As for Components, they must remain plain and inert data, meaning that they do not hold any logic whatsoever. No methods, no constructors, no destructors, no properties, no events, no nothing except public fields. This ensures that Components are easy to understand, predictable, easy to serialize and have a good memory layout. This might be surprising and/or worrying for a mind that is used to control the access to their object's data with properties and/or methods but I'm telling you that as soon as you let go of the idea of protecting/hiding data you eventually realize that it was not strictly necessary and that everything is actually alright.

Since Entities are just identifiers and Components are just inert chunks of data, we need something to actually does something in this program. Systems are the last missing piece. They are conceptually the equivalent to a function that take all the existing Entities and Components, filters the ones it is interested in and processes them in some way. This means that ideally, Systems do not hold any state since all the game state exists exclusively within Components. I say ideally because for optimization purposes, some System local state may be needed.


More Concepts

Entities, Components and Systems are the minimal set of concepts that the framework needs to be useful, but additional ones have been added for convenience and performance. I will very briefly expose them here, but each of these will be described in much more details in the wiki.

  • World: the collection of all existing Entities and Components. It is the giant block of data that manages all the other blocks of data. The World is also the basis for extensibility because it holds the framework's modules and allows anyone to add one. This makes the framework extremely adaptable to any game development setup.
  • Message: a temporary Component that is not attached to an Entity and that simplifies System communication.
  • Resource: a World-wide Component that is not attached to an Entity that holds global data.
  • Group: a list of all the Entities in a given World that conform to a query.
  • Node: a data wrapper around Systems that allow to define execution behavior and order.
  • Controller: a wrapper around Systems that executes them based on the behavior defined in nodes and that controls their state.
  • Phase: a data type that is associated with a phase of execution. It allows to run systems at different times such as initialization time, run time, dispose time or any other time that you care to define.

Recurrent Usage Patterns

I will specify here recurrent usage patterns that are used in the framework.

When in doubt, use a struct.

  • Almost everything you will implement when using the framework will be a struct. Entities are structs, Components are structs, Systems are structs and other concepts such as Messages, Resources, Queryables and Injectables are all structs.
  • The framework will enforce the usage of structs.
  • This (almost abusive) usage of structs is deliberate.
    • Structs are great for cache locality and as such will allow the CPU to access them much more quickly than reference types.
    • ECS is all about favoring composition and structs enforce this idea since they prevent any kind of OOP inheritance impulses that one could have.
    • Structs correspond much more appropriately to plain and inert data.
    • Structs don't require useless indirection and null checking when accessing members.
    • The cost of passing (copying) large structs around is nullified by C#7's ref returns.

Most concepts have an empty associated interface.

  • Components must implement IComponent, Systems must implement ISystem, Messages must implement IMessage and so on.
  • These interfaces are all empty but they enforce users to be explicit about their intent since all data would otherwise look alike.
  • The framework will enforce that you use the correct interface with the appropriate functionality.

Most things are extensible.

  • Whether you want to add a new way to query Entities or add you custom serialization module, there is a way to extend the framework to accommodate you.
  • The whole framework is implemented as extensions to the World which means that it is so extensible that you could, in principle, implement another ECS within Entia.
  • Most extensions use interfaces and/or attributes to allow efficient, flexible and AOT (ahead of time compilation) friendly extensions.
    • For example, to implement a new kind of query, you would have to define a struct that implements the IQueryable<T> (an empty interface) where T is an IQuerier and define a type that implements IQuerier (a non-empty interface that builds queries).
    • This interface-linking pattern (an interface that has for main purpose to link a type T) is used a lot in the framework. It makes it explicit what the default implementation for concepts is and ensures that those linked types are properly AOT compiled even when using generic types.
    • AOT support is essential since some target platforms (such as iOS) require it.
  • Most existing implementations can be replaced with your own.
    • If you don't like how the framework parallelizes your Systems, you can replace the threading model by your own.
    • Most modules are implemented as a map between a specification type and an implementation interface and expose a Set method such that implementations can be replaced.
  • It is to be noted that extensions may require a fair amount of knowledge about how the framework works to make them work properly. I have tried to make relatively small modules such that extending one doesn't require too much knowledge, but still consider this as an advanced feature.