Entity/Component Authoring

I’d like to offer people a “magic” way of doing entity/component authoring that cuts out all the boilerplate. However, some of what I want to do is maybe antithetical to the spirit of ECS so it’s proving to be a philosophical moment of trying to figure out how much ECS I want to expose to people that actually use the engine.

Specifically, the “ECS” way to do things would be to provide basically 0 baseline entity functionality and let people define their own component/entity relationships. Then, also let people define their own systems to interact with that. So something like

struct position (float x, float y)
struct velocity(float dx, string dy)
var e = Engine.ECSWorld.Create(
    new position(),
    new velocity(this)
);

public class VelocitySystem : DSystem, IUpdateSystem
{
    QueryDescription query = new QueryDescription().WithAll<Active,HasManagedOwner,Position, Velocity>();
    public void Update(double dt)
    {
        Engine.ECSWorld.Query(in query, (Arch.Core.Entity e, ref Position pos, ref Velocity vel) => {
            pos.x += vel.x;
            pos.y += vel.y;
        });
    }
}

However, I think the systems-cetric way of defining behavior is onerous for rapid game creation stuff. A nice thing is to tick the entity directly on Update like:

void Update() {
      e.pos.x += e.vel.x;
      e.pos.y += e.vel.y;
}

However, entity creation in the first method wouldn’t allow the exposing of the pos field on entity (the entity is just a basic struct in Arch, Dinghy’s underlying ECS framework). You’d have to instead access it like:

void Update() {
   e.Set(new Position( X = 10 ));
}

Which also sucks from the “immediacy” principle of Dinghy.


This considered, what I’m thinking of doing it trying to emphasize Update-style logic for users, with an ECS backend that is more opt-in but at the highest API level is basically transparent. However, because I am still using the ECS to run things, we need a way to basically “build” the right ECS objects and fields automagically.

My main effort in this area has been looking at ways to type-safedly author entites in a seperate data-description language. I’ve looked at a lot of them (like Dhall and Jsonnet) and landed on what I think is a really great option, Cue.

Skipping to the end with a note that it would be great to talk about Cue in a seperate post (I think Dinghy may use it a lot more than just entity defining!), the idea is that you basically have a .cue file in your project directory that has something like this in it:

package entities

import (
	Components "afterschool.studio/components"
)

#Sprite : #Entity & {
    Renderer : Components.#SpriteRenderer
    Position : Components.#Position
}

And then at edit time that file gets automatically converted into something like this:

public class Sprite : BaseEntity
{
    private float x = 0;
    public float X
    {
        get => x;
        set
        {
            ref var pos = ref ECSEntity.Get<Position>();
            pos.x = value;
            x = value;
        }
    }

    private float y = 0;
    public float Y
    {
        get => y;
        set
        {
            ref var pos = ref ECSEntity.Get<Position>();
            pos.y = value;
            y = value;
        }
    }
//...etc.

Then also you can have a file that has explict instances of this (still just in Cue) like this:

package root

import (
	Entities "afterschool.studio/entities"
)

ally: Entities.#Sprite & { Renderer:Path: "ally.png" }

Which would then in C# give you the immediate ability to do something like:

var ally = new Ally();

And it would Just Work.

So you have a lightweight entity-authoring environment that doesn’t require you to hand-write C# entity classes that are basically only shims. The other killer thing about Cue is that it is a proper language, so if you don’t define an entity properly at edit time it will throw data errors. I.e. Doing this:

package root

import (
	Entities "afterschool.studio/entities"
)

//we're leaving out the ally.png path
ally: Entities.#Sprite
//working version for reference
//ally: Entities.#Sprite & { Renderer:Path: "ally.png" }

And trying to export the data will give you a validation error:

ally.Renderer.Path: incomplete value string:
    ./cue.mod/pkg/afterschool.studio/components/SpriteRenderer.cue:5:12

So you can be sure at edit time that your entity configs are correct and don’t rely on build time processes. There are also plans to add in a LSP to things like VS Code so you’d see these errors as you work on the data!


I’m really feeling this Cue direction as a way to do baseline “correct” entity definition and then letting users basicalyl work with entites directly like a “normal” game engine. Then if people want to go the ECS route, they already have the component definitions assigned and can do systems more directly if that’s what they like. It feels like (hopefully) the best of both worlds, but I just need to get the Cue stuff setup first so we can start testing it out and validating.

Interested to hear any thoughts here!