Giter Club home page Giter Club logo

softtouch.ecs's Introduction

SoftTouch.ECS

This project is lightweight ECS implementation with archetypal storage, heavily inspired by FLECS. The API aims to be very fast and allocation free for queries and component updates.

Usage

At the moment the library is not distributed on nuget so you still have to pull it from git and reference it in your project.

The api is very similar to bevy-ecs but the naming conventions is inspired from Stride3D's.

API

World and entities

Everything starts with the creation of a World object. Worlds manage their entities, storages for components and processors. You usually use it through the App object.

var app = new App();

Once you have a world you can spawn entities either with components or without.

app.World.Commands.Spawn();
app.World.Commands.Spawn(new Name("John Doe"), default(Transform), (1,"some text"));
app.World.Commands
    .Spawn()
    .With(Name("Jane doe"))
    .With(("mochi",5,true));

Components

Components are stored in List<T>. To make sure components are not allocated individually on the heap, they are constrained to be structs. This both avoids fragmentation and make sure iterating over them is made very fast.

Using the code above we could have defined our components this way :

public record struct Name(string Value)
{
    public static implicit operator string(Name n) => n.Value
}


public record struct Transform(Vector3 Position, Quaternion Rotation, Vector3 Scale);

Systems/Processors

In this library, the S in ECS has been renamed to Processor, this was a choice influenced by Stride's naming convention and also because I personally feel System is very vague for what this implementation is really.

To create Systems/Processors you can either create it from a class implementation

public class MyStartupProcessor : Processor<Commands, Resource<MyResource>>
{
    public override void Update()
    {
        var commands = Query1;
        commands.Spawn(new NameComponent("Jane doe"), 5, new HealthComponent(100,100));
        commands.Spawn();
    }
}

public class MyFirstProcessor : Processor<Query<Name,Transform>>
{
    public override void Update()
    {
        foreach(var e in Query)
        {
            // Here goes your logic
        }
    }
}

Or create a static function

public static void MyFirstSystem(Query<Name,Transform> entities)
{
    // Here goes your logic
}

And then add it to the world and you can start updating your frames!

app.AddProcessor<MyFirstProcessor>();
app.AddStartupProcessor<MyStartupProcessor>();
// world.AddProcessor(new MyFirstProcessor());

app.AddProcessor(
    (Query<Name,Transform> q1) => MyFirstSystem(q1)
);

for(int i = 0; i < 100; i++)
    world.Update();

Iterating over entities

This is the meat and potatoes of this library. Iterating over entities can be done in many different ways but for that there needs to be an explanation about how component storage works in this library.

Entities are just indices linked to Archetypes. They are stored in the world as a list.

Entities are grouped together based on which group components they hold. Those groups are called Archetypes. When you spawn an entity with components, the world checks if this entity can fit in an existing Archetype or has to generate a new one. You can also add a component to an entity and the world will wait for the end of a frame to move the entity to another Archetype and add the corresponding component data.

Archetypes contain a Storage value which has a type ressembling Dictionary<Type,List<T>> and an index redirection to tell which Archetype index corresponds to which entity index.

As a bare bone implementation, you could iterate over those Archetypes yourself and select which entities you want to work with like so :

public class MyFirstProcessor : Processor<Query<Name,Transform>>
{
    public override void Update()
    {
        // Processors created with a class have access to the world, so you can access pretty much any storage from there
        World.Archetypes.Values[0].Storage[typeof(Name)][0] = new Name("Ada Lovelace");
    }
}

This is a very versatile way of querying the world, you get to the data you need, but it's mouthful and not really good for performances.

Processors have Query fields that contains helper methods and iterators to help you iterate over entities and their components. When using iterators you constrain your logic to the types you have chosen to work with. This makes it easier for the system to avoid processors accessing the same chunks of memory at the same time, to avoid cache misses.

// Here the processor queries over entities that have a Name and Transform components
public class MyFirstProcessor : Processor<Query<Name,Transform>>
{
    public override void Update()
    {
        // Query is a field helping you with iterators
        // The 1 is because you can have up to 4 queries in a processor if you want to iterate over two different list of entities
        // e.g. an entity with a mesh component and another with a camera component
        foreach(var entity in Query)
        {
            // The entity here can be deconstructed into the components queried
            var (name, transform) = entity;
            // entity also has a method to set a component. It can be one you queried, or another that you know exists but haven't queried
            // There also is a Get<T> method as well as a Has<T> method to help you make safe code
            if(name == "John Doe")
                entity.Set(new Name("Ada Lovelace"));
        }
    }
}

Scheduling (WIP)

The implementation offers a processor scheduler. Processors can be declared in ordered stages, you can execute logic for input events before the game logic by creating stages and ordering them.

Processors with disjoint queries (i.e. that queries completely different components) are grouped together in stages in order to be run in parallel.

var app =
    new App()
    // This startup processor will be run during the `Startup` stage
    .AddStartupProcessor<StartupProcessor>()
    // These processors will be run during the default `Main` stage, you can specify the stage by adding the optional string parameter name
    .AddProcessor<SayHello>()
    .AddProcessor<WriteAge>();

// Upon update, SayHello and WriteAge will run in parallel since they both query different types in the database.
app.Update();

Given these processors :

public class StartupProcessor : Processor<Resource<WorldCommands>>
{
    public StartupProcessor() : base(null!)
    {

    }
    public override void Update()
    {
        Random rand = new Random();
        WorldCommands commands = Query;
        for(int i = 0; i < 1000; i++)
        {
            commands.Spawn(rand.Next(1,100), new NameComponent($"john n°{i}"));
        }
    }
}

public class WriteAge : Processor<Query<Read<int>>>
{
    public WriteAge() : base(null!) { }
    public override void Update()
    {
        foreach(var entity in Query)
        {
            Console.WriteLine($"There's a person that is {entity.Get<int>()} years old");
        }
    }
}
public class SayHello : Processor<Query<NameComponent>>
{
    public SayHello() : base(null!) { }
    public override void Update()
    {
        foreach (var entity in Query)
        {
            Console.WriteLine($"Hello {entity.Get<NameComponent>().Name}!");
        }
    }
}

F# API

The F# api covers a subset of the C# api but it is designed to be very friendly to functional programming.

Here's an example :

open SoftTouch.ECS
open SoftTouch.ECS.FSharp
open SoftTouch.ECS.Querying

[<Struct>]
type NameComponent = 
    val mutable Name : string
    new (n : string) = {Name = n}
    override this.ToString() = $"{this.Name}"
    

let app = new App()


let startup (commands : Commands) =
    commands
    |> Commands.spawn
    |> Commands.WithValue (NameComponent "No Name")
    |> ignore
    

let nameSystem (entities : Query<NameComponent>) : unit =
    for entity in entities do
        entity.Get<NameComponent>().Name
        |> printfn "original name is : %s"


        let name = NameComponent "Kujo Jolyne"
        entity.Set(&name)

        entity.Get<NameComponent>()
        |> printfn "Changed to %A"

let x = 0;

app
|> Processor.AddStartup startup
|> Processor.Add nameSystem
|> App.update
|> App.update
|> (fun app -> app.World)
|> World.getEntity 0 
|> Entity.Get<NameComponent>
|> fun x -> x.Name
|> printfn "Hello %s"

softtouch.ecs's People

Contributors

ykafia avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

softtouch.ecs's Issues

Nuget package?

Title says it all. This project of yours could easily prevent me from the boilerplate of spinning up my own ecs. The icing on the cake would be nuget for automatic updates as you continue to improve the library here :).

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.