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.
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.
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 */ }
}
}
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:
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.
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.
I will specify here recurrent usage patterns that are used in the framework.
struct
.struct
. Entities are structs, Components are structs, Systems are structs and other concepts such as Messages, Resources, Queryables and Injectables are all structs.ref
returns.interface
.IComponent
, Systems must implement ISystem
, Messages must implement IMessage
and so on.interface
with the appropriate functionality.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).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.interface
and expose a Set
method such that implementations can be replaced.