Extensible ECS data structure

Hi. I am trying to make a ECS library and I’m struggling with the data structure.

Here’s my current structure as an example. It works, but it’s not extensible.
The data field of the Component type is what’s causing trouble as it needs to represent all possible component data, which changes depending on the project.

type alias Scene =
    { entities : List Int
    , components : List Component
    , systems : List (Scene -> Scene)
    }


type alias Component =
    { id : Int
    , parent : Int
    , data : ComponentData
    }


type ComponentData
    = Name String
    | Player
    | Health { current : Float, max : Float }

Has anyone done something similar to this or have any insights? I have looked at elm-ecs, but it seems complicated, I’m trying to make something simple if possible.

2 Likes

This talk talks about writing an ECS. You may find some insights there.

Sadly that talk is more of an overview. I did poke aroud the source code though, seems like he’s using something called specs. Guess I need to find out how that works :confused:

Hi,

Welcome to Elm Discourse.

How about something like this?

type alias Scene data =
    { entities : List Int
    , components : List (Component data)
    , systems : List (Scene -> Scene)
    }


type alias Component data =
    { id : Int
    , parent : Int
    , data : data
    }


type ComponentData
    = Name String
    | Player
    | Health { current : Float, max : Float }

When you define your Scene type on your Model you’d do the following:

type alias Model =
    { scene : Scene ComponentData 
    , ...
    }

Now you can apply functions to your list of components because the compiler knows what type the type variable data is.

Just a thought…

p.s. Did you make a typo in your Scene type because it’s recursive and so won’t compile.

1 Like

That works, and it’s simple. Thank you so much!

Regarding the typo, you’re right. I typed it directly in here and slightly messed up.
Here’s the working code, I think it’s super cool that a basic ECS can be this small :slight_smile:

type alias World data =
    { entities : List Int
    , components : List (Component data)
    , systems : List (List (Component data) -> List (Component data))
    }


type alias Component data =
    { id : Int
    , parent : Int
    , data : data
    }

Thanks again for your help.

1 Like

No problem, glad it worked for you.

Hi @panda , isn’t an ECS mainly valuable to control the memory representation and locality of data needed to be used together? Is there still a value proposition for a language like Elm where memory is completely out of our control?

Yes, that is a big upside that is probably lost, I have no idea how JS memory allocation works to be honest.

I am mainly interested in composability for small games however, and I think the ECS pattern fits that purpose well.

1 Like

At the low level, ECS does provide some performance benefits.

But at a higher level, ECS allows for a powerful form of ad-hoc property (aka component) composition. It’s not the right answer for every game but when it works it works well.

If you have a sword you can easily add a “flame damage” buff by just attaching that component to the relevant entity. Debuffs work the same, as well as unique items or enemies etc…

2 Likes

Out of curiosity, what is id for in Component? I assume parent is the entity id?

I’ve used elm-game-logic 3.0.0 in a game jam game. It is useful too, once you get the flow of what it is doing.

Another option I explored is the ECS used in the mogee game : elm-mogee/src at main · w0rm/elm-mogee · GitHub You have to piece together what the ECS is since it is custom for the game, but peeking at the Components and Systems folders gives you nice examples of its use in a released game :smiley:

BTW if you want there is a bunch of game devs in #gamedev in the Elm slack and we love seeing devlogs and people’s progress

I use the id to update/remove components. I haven’t figured out a way of making it work without it.

Thanks for the links, I will check them out. :smiley:

I see! If you want to keep things even simpler, maybe you can use the whole component to compare it directly:

> type alias Component data =
|       { id : Int
|       , parent : Int
|       , data : data
|       }
> a = Component 1 2 True
{ data = True, id = 1, parent = 2 }
    : Component Bool
> b = Component 1 2 True
{ data = True, id = 1, parent = 2 }
    : Component Bool
> a == b
True : Bool

In Elm comparisons are by value :slight_smile:

I did try this, but couldn’t figure out how to pass Component data around as an argument, it all got a bit messy.

Also a == b vs a.id == b.id is not that big of a deal for me.

The IDs are sensible. For example, if you have an entity that somehow needs to know another entity (say an enemy that follows another entity) you don’t want to keep a copy of the referenced entity with the referencing entity because that would require updating all references (and to find them: iterating over all entities) whenever an entity changes one of its properties (for example, its position).

Also, if you have messages that reference entities by value instead of by ID, you have to be very careful with outdated messages.

(You probably want to introduce an opaque type for the IDs at some point, though, instead of using plain Ints.)

Regarding the opaque type, you are right. I already use one, if I understand opaque types correctly.

If anyone is curious, this is the entire ECS at the moment: https://gist.github.com/IsakUlstrup/314e0c91e52d95279d9abcd688f76f56

All my focus has been on components, so the systems are basically placeholders.

I really appreciate this discussion by the way. You Elm people are awesome! :heart:

So I went ahead with this solution, but I’m super stuck on how I actually read data from Component.data.
It seems Elm knows whats going on, the types work as expected. But I can’t pattern match or get anything out of that field unless I use Debug.toString.

I don’t think I fully understand this pattern.

Can you show what you are attempting?

If you were just using a String for your data type, then you should be able to recurse over your list of components and use String functions on your data field. What is your data type?

(I need to pop out in a mo, so if no-one else is able to help before I get back, I’ll try and help further later on.)

Just a quick thought, if you are implementing this as a stand-alone library, then the library itself won’t be able to work with the data field, because it has no knowledge of what the data type is.

Only the consumer of the library, that provides the type to your World will be able to because the consumer provided the type.

Although the functions the consumer provides to the systems field should be fine.

Ecs.elm

type alias World data =
{ entities : List Entity
, components : List (Component data)
, systems : List (System data)
, idCounter : Int
}


type alias Entity =
Int


type alias ComponentId =
Int


type alias Component data =
{ id : ComponentId
, parent : Entity
, enabled : Bool
, data : data
}


type alias System data =
... not important

Main.elm

type alias Model =
{ world : World ComponentType
, dt : Float
}

type ComponentType
= Name String
| Player
| Health { current : Float, max : Float }
| Position Vector2
| Velocity Vector2

renderComponent : Component data -> Html Msg
renderComponent component =
    let
        renderComponentData : ComponentType -> Html Msg
        renderComponentData cd =
            case cd of
                Name name ->
                    text name

                Position position ->
                    text ("x: " ++ String.fromFloat position.x ++ " y: " ++ String.fromFloat position.y)

                _ ->
                    text "..."
    in
    Html.li []
        [ p [] [ text (Debug.toString component.id) ]
        , p [] [ renderComponentData component.data ]
        ]

I would kinda expect this to work, but it fails with this error on the second to last line:

The value at .data is a:

#data#

But `renderComponentData` needs the 1st argument to be:

#ComponentType#

If I change renderComponentData to expect data I get the reverse error.