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.
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
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.
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
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
}
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?
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…
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
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 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
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.)
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.
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.
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.