A lightweight and easy to use entity component system with an effective feature set for making games.
World
// A world is a container for different kinds of data like entities & components.Worldworld=new World();
Entity
// Spawn a new entity into the world.Entityentity= world.Spawn();// Despawn an entity.
entity.Despawn();
Component
// Components are simple classes.classPosition{publicintX,Y;}classVelocity{publicintX,Y;}// Add new components to an entity.
entity.Add<Position>().Add(new Velocity { X =1, Y =0});// Get a component from an entity.varvel= entity.Get<Velocity>();// Remove a component from an entity.
entity.Remove<Position>();
Element
// Elements are unique class-based components that are attached directly to worlds.classSavePath{stringValue;}// Add an element to the world.// You can only have one element per type in a world.
world.AddElement(new SavePath( Value ="user://saves/"));// Get an element from the world.
var savePath = world.GetElement<SavePath>();
Console.WriteLine(savePath.Value);// Remove an element from the world.
world.RemoveElement<SavePath>();
Relation
// Like components, relations are classes.classLikes{}classOwes{publicintAmount;}classApples{}varbob= world.Spawn();varfrank= world.Spawn();// Relations consist of components, associated with a "target".// The target can either be another component, or an entity.
bob.Add<Likes>(typeof(Apples));// Component ^^^^^^^^^^^^^^
frank.Add(new Owes { Amount =100}, bob);// Entity ^^^// You can test if an entity has a component or a relation.booldoesBobHaveApples= bob.Has<Apples>();booldoesBobLikeApples= bob.Has<Likes>(typeof(Apples));// Or get it directly.// In this case, we retrieve the amount that Frank owes Bob.varowes= frank.Get<Owes>(bob);
Console.WriteLine($"Frank owes Bob {owes.Amount} dollars");
Commands
// Commands are a wrapper around World that provide additional helpful functions.Commandscommands=new Commands(world);// You *do not* need to create your own commands.// They will be automatically provided for you as the System.Run(Commands) argument.
Query
// With queries, we can get a list of components that we can iterate through.// A simple query looks like thisvarquery= commands.Query<Position,Velocity>();// Now we can loop through these componentsforeach(var(pos, vel)in query){
pos.Value += vel.Value;}// You can create more complex, expressive queries.// Here, we request every entity that has a Name component, owes money to Bob and does not have the Dead tag.varappleLovers= commands.Query<Name>().Has<Owes>(bob).Not<Dead>();// Note that we only get the components inside Query<>.// Has<T>, Not<T> and Any<T> only filter, but we don't actually get T int he loop.foreach(var name in query){
Console.WriteLine($"{name.Value} owes bob money and is still alive.")}
System
// Systems add all the functionality to the Entity Component System.// Usually, you would run them from within your game loop.publicclassMoveSystem:ISystem{publicvoidRun(Commandscommands){// Query desired components.varquery= commands.Query<Position,Velocity>();// Loop over queried of components.foreach(var(pos, vel)in query){
pos.Value += vel.Value;}// You can also access the entity within the loop.varquery= commands.Query<Entity,Position,Velocity>();foreach(var(entity, pos, vel)in query)=>{
pos.Value += vel.Value;// Example: "Tag" a component to show that it has moved.
entity.Add<Moved>();}}}
Running a System
// Create an instance of your system.varmoveSystem=new MoveSystem();// Run the system.// The system will match all entities of the world you enter as the parameter.
moveSystem.Run(world);// You can run a system as many times as you like.
moveSystem.Run(world);
moveSystem.Run(world);
moveSystem.Run(world);// Usually, systems are run once a frame, inside your game loop.
Triggers
// Triggers are also just structs and very similar to components.// They act much like a simplified, ECS version of C# events.classMyTrigger{}// You can send a bunch of triggers inside of a system.
commands.Send<MyTrigger>();
commands.Send<MyTrigger>();
commands.Send<MyTrigger>();// In any system, including the origin system, you can now receive these triggers.
commands.Receive((MyTrigger e)=>{
Console.WriteLine("It's a trigger!");});// Output:// It's a trigger!// It's a trigger!// It's a trigger!// NOTE: Triggers live until the end of the next frame, to make sure every system receives them.// Each trigger is always received exactly ONCE per system.
SystemGroup
// You can create system groups, which bundle together multiple systems.SystemGroupgroup=new SystemGroup();// Add any amount of systems to the group.
group.Add(new SomeSystem()).Add(new SomeOtherSystem()).Add(new AThirdSystem());// Running a system group will run all of its systems in the order they were added.
group.Run(world);
Example of a Game Loop
// In this example, we are using the Godot Engine.using Godot;using RelEcs;using World = RelEcs.World;// Godot also has a World class, so we need to specify this.publicclassGameLoop:Node{Worldworld=new World();SystemGroupinitSystems=new SystemGroup();SystemGrouprunSystems=new SystemGroup();SystemGroupcleanupSystems=new SystemGroup();// Called once on node construction.publicGameLoop(){// Add your initialization systems.
initSystem.Add(new SomeSpawnSystem());// Add systems that should run every frame.
runSystems.Add(new PhysicsSystem()).Add(new AnimationSystem()).Add(new PlayerControlSystem());// Add systems that are called once when the Node is removed.
cleanupSystems.Add(new DespawnSystem());}// Called every time the node is added to the scene.publicoverridevoid_Ready(){// Run the init systems.
initSystems.Run(world);}// Called every frame. Delta is time since the last frame.publicoverridevoid_Process(floatdelta){// Run the run systems.
runSystems.Run(world);// IMPORTANT: For RelEcs to work properly, we need to tell the world when a frame is done.// For that, we call Tick() on the world, at the end of the function.
world.Tick();}// Called when the node is removed from the SceneTree.publicoverridevoid_ExitTree(){// Run the cleanup systems.
cleanupSystems.Run(world);}}