Write-up on entity component systems in Elm

383 views
Skip to first unread message

Hassan Hayat

unread,
Dec 29, 2014, 6:50:40 PM12/29/14
to elm-d...@googlegroups.com
Hi there,

I've been exploring Entity Component System as an architectural pattern in Elm and documented my experience in this gist. It is a bit long, perhaps a bit boring, and has quite a bit of code (as in, small but complete examples: 100+ lines of code) but I felt like I had to because it was about code architecture and hello world doesn't cut it and also I wrote it as I was working on the examples. So, please forgive any typos and grammar issues.

Basically, the examples involve making squares move with the keyboard and then seeing what happens to the code if you add squares that you don't want to move with the keyboard, and then what happens if you try to add gravity, and what about adding circles, etc...

It must be noted that I'm still in the process of learning functional programming and was looking into this architectural pattern because I came across a few stumbling blocks while working on my game in Elm. 

Basically what happened is that I came across a moment when I wanted to add a feature but that that feature involved substantial changes to the codebase and that I had to work hard to get around the fact that you can't have a heterogeneous list in Elm. It led me to write lots of code that looks very similar.

The TL;DR of the writeup is that Entity Component System lends itself more to modifying the code than using records as in the examples. It is basically the plugin model. An entity is a list of components. A component is just some data. And the system applies some "actions" which are functions that modify the components of entities. 

type Entity = Entity (List Component)

type
Component =
 
Position Float Float |
 
Velocity Float Float

fixed = Entity [
     
Position 0 0
 
]

moving
= Entity [
     
Position 20 0,
     
Velocity 1 2
 
]

This allows you to just slam all your entities into one big list (even if they have different components):

entities = [fixed, moving]

and then you can just create function to operate on individual or multiple components.

moveEntity : Entity -> Entity
moveEntity entity
=
 
case (getPosition entity, getVelocity entity) of
   
(Just (Position x y), Just (Velocity vx vy)) ->
       updatePosition
(Position (x + vx) (y + vy)) entity
    _
-> entity

where getPosition gets the position component of an entity if it exists, getVelocity gets the velocity component of an entity if it exists, and updatePosition updates the position component of an entity if it exists

getPosition : Entity -> Maybe Component
getVelocity
: Entity -> Maybe Component
updatePosition
: Component -> Entity -> Entity

The only big problem that I've found with this approach is that it requires a bit of boilerplate to do well. Basically, for every component you add, you need to provide a get, update, and filter function in order to do anything interesting with entities. There may be better ways to do this but I couldn't think of any and I've outlined a couple of ways in the piece of how to mitigate this problem.

I don't know how interesting this may be but I felt like sharing this experience of using Elm and trying to find ways to write better and more flexible apps in Elm,
Hassan Hayat





Jeff Smits

unread,
Dec 30, 2014, 5:39:00 AM12/30/14
to elm-discuss
I think it's laudable how you approach these problems. It's really great that you run into a problem and write down your process towards solving it for our benefit. Thanks for sharing! 

I've quickly read the gist without looking too deeply into the code and I didn't find it boring. At the point where you switch to the Entity Component System (ECS), my first reaction was it's not really necessary. You could have changed field size : Float to shape : Shape or shape : MyShape where type MyShape = Box Float | Circle Float. Then you'd still have one Entity to put in the list. 
But then you cleverly grow a largely orthogonal feature set and this is where it becomes clear that maybe showing almost everything in the type is not scalable. I'm not completely comfortable with this union type to prevent the type system from understanding what you're doing, but I don't see a better way at the moment. 

I think you can get rid of most of the boilerplate if you use a record with Maybe fields instead of a List of the union type. Unless you want to be able to specify a Component multiple times, but that doesn't seem to fit in your current style. So this way the type system helps prevent stuff like that again :)
So change:

type Component =
  Position Float Float |
  Velocity Float Float |
  Mass Float |
  Scale Float |
  Color Color |
  Shape Shape |
  Controllable |  
  Static

to

type alias Component = {
  position : Maybe Vector
, velocity : Maybe Vector
, mass     : Maybe Float
, scale    : Maybe Float
, color    : Maybe Color
, shape    : Maybe Shape
, controls : Maybe Controls -- looks nicer than controllable : Maybe ()
}

Now you have free getters. Filtering is something you should reuse the normal List.filter for, combined with a getter and isJust. 
As for updates, that's a little more annoying because there is no .field syntax for updating. But you could write the boilerplate as a Focus

Another thing you can make a little nicer is functions like these:

moveEntity : Entity -> Entity
moveEntity entity =
  case (getPosition entity, getVelocity entity) of
    (Just (Position x y), Just (Velocity vx vy)) ->
      updatePosition (Position (x + vx) (y + vy)) entity
    _ -> entity


With some helper functions, you can change them to:

moveEntity entity = update2 entity position (maybeMap2 vAdd) position velocity

(helper functions: )

maybeApply : Maybe (a -> b) -> Maybe a -> Maybe b
maybeApply mf ma = mf `Maybe.andThen` (\f -> Maybe.map f ma)

maybeMap2 : (a -> b -> c) -> Maybe a -> Maybe b -> Maybe c
maybeMap2 f m1 m2 = f `Maybe.map` m1 `maybeApply` m2

position = Focus.create .position (\f e  -> { e | position <- f e.position })
velocity = Focus.create .velocity (\f e  -> { e | velocity <- f e.velocity })

update1 : thing -> (Focus thing value) -> (value1 -> value) -> (Focus thing value1) -> thing
update1 thing fSetter calculation fGetter1 = set fSetter (calcutation (get fGetter1 thing)) thing

update2 : thing -> (Focus thing value) -> (value1 -> value2 -> value) -> (Focus thing value1) -> (Focus thing value2) -> thing
update2 thing fSetter calculation fGetter1 fGetter2 = set fSetter (calcutation (get fGetter1 thing) (get fGetter2 thing)) thing

--
You received this message because you are subscribed to the Google Groups "Elm Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elm-discuss...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Hassan Hayat

unread,
Dec 30, 2014, 8:33:03 AM12/30/14
to elm-d...@googlegroups.com
Thanks for the reply. I'm glad you didn't find it boring. 

The idea of using records with maybe is interesting. It's true that records are much easier to update. The weird thing is having every entity in the system actually have those fields (even if they're set to Nothing). It certainly works, but it feels weird.

Anyways, in that case, there would be a strict convention on how to create entities.

First of all, I think it would be better to call the record type "Entity" rather than "Component". We don't need every component to potentially know about every other component. That said, I have the impression from the rest of your code that this is what you meant.

So, to create a default entity:

entity : Entity
component
= {
  position
= Nothing,
  velocity
= Nothing,
  mass    
= Nothing,
  scale    
= Nothing,
  color    
= Nothing,
  shape    
= Nothing,
  controls
= Nothing
}

we could then create entities like so: 

redBox =
 
{ entity | position <- Just (Vector 10 10),
            velocity <- Just (Vector 1 0),
            mass
<- Just 10,
            scale
<- Just 10,
            shape
<- Just Square,
            controls
<- Just ArrowControls  }
 

All the Justs feel odd. But they're not too bad in my opinion. 

One could imagine creating a DSL with just a bit of boilerplate to create these entities where it goes like

greenBall =
  entity
<> [
    position
40 10,
    velocity 10 -10,
    mass
20,
    scale
10,
    color
0 255 0,
    shape Circle ]



-- where each of the things in the list a functions from Entity to Entity (updating the default entity, basically).
-- so, for example:
position
: Float -> Float -> Entity -> Entity
position x y entity =
  {entity | position <- Just (Vector x y) }


-- thus, our little diamond operator could be
(<>) : Entity -> List (Entity -> Entity) -> Entity
(<>) entity updaters =
 
case updaters of
   
[] -> entity
    f
:: fs -> (<>) (f entity) fs
 

Just a few ideas I'm throwing around to make things easier.

Hassan Hayat

unread,
Dec 30, 2014, 8:53:27 AM12/30/14
to elm-d...@googlegroups.com
I've just created a gist (not a write up, just code) where I crystalized some of those ideas.

Link to gist: https://gist.github.com/TheSeamau5/9f885fe62b2c776cdd26

Hassan Hayat

unread,
Dec 30, 2014, 9:01:35 PM12/30/14
to elm-d...@googlegroups.com
Here. I think this gist has a close to complete expression of an entity component system. The main features are:

1) You can add new components and no old code will break
2) You can add new actions and no old code will break
3) You can have entities composed of wildly different components and everything just works
4) The system takes into account input through the type (although, currently the type must be extended manually)
5) The actions operate on entities but all are passed in the input and the world. This means that collision detection is now possible in this model.

Drawbacks:

1) Every entity must have as many fields as there are types of components
2) Input is currently handled manually. A more general solution must be found.
3) Everytime you want to add a new entity, you must create its correspondent helper function. But the price is worth it as you gain a great syntactic affordance which pays off since you tend to create more entities than you create components.
4) Overreliance on the List type. While a move to Array is possible, it is hard to imagine mixing data structures depending on the need.

Link to gist: https://gist.github.com/TheSeamau5/8d7fac9f1937f0bab317
Reply all
Reply to author
Forward
0 new messages