Upside down object inheritance in Elm

type alias Body =
    { id : Int
    , mass : Float
    , position : Vector2D
    , velocity : Vector2D
    , radius : Float
    , bodyType : BodyType
    }


type BodyType
    = Conceptual 
    | Planet { gravity : Float }
    | Ship
        { rotation : Float
        , rotationSpeed : Float
        , propulsion : PropulsionType
        }
    | Projectile
        { damage : Int
        , lifetime : Float
        }


type PropulsionType
    = Newtonian { thrust : Float }
    | LittleGrayMenTech { movementIncrement : Float }

One of the obstacles with game dev in FP languages have reportedly been that it’s hard to model game loops without OO since so many actions rely on altering various similar aspects of the various game entities.

In making Space Pew Pew, I hit a bit of a wall early on wrt making records that have similar fields (everything you see in Body), because whenever I needed to update the game state, I had different lists of entities (like ships, planets, projectiles, etc). I had to extract the similarities, then update the separate lists again. This immediately looked like it was going to be a tonne of code.

So what Claude and I came up with is what you see above. If there’s a name for this pattern (or anti-pattern), please let me know.

Basically I start with the common fields, then add a field which links to the specialization information that might be available. So everything is a body with position, velocity, etc. Then some of the things are planets or ships or projectiles. Then the ships have varying means of propulsion, like rocket based Newtonian means or LGM tech which has them move without gaining velocity.

This allows me to do stuff like this:

ship_propel : Body -> Body
ship_propel body =
    case body.bodyType of
        Ship ship ->
            case ship.propulsion of
                Newtonian { thrust } ->
                    body |> applyForce (angleToV thrust ship.rotation)

                LittleGrayMenTech { movementIncrement } ->
                    { body
                        | position = addV body.position (scaleV movementIncrement (angleToV 1 ship.rotation))
                        , velocity = { x = 0, y = 0 }
                    }

        _ ->
            body

So I can call ship_propel on any body and it will either apply the propulsion logic and give me a new record, or just leave the record as is.

Would love to hear the community’s thoughts on this, especially if I might it a wall with this approach. The game structure is going to get about 10 times more involved than it is right now, so your feedback is valuable.

3 Likes

Yes, that would work. If you have few, disparate game entities this is how I would do it.

Another popular approach, developed within the video game industry, is ECS (Entity, Component and System). In other words: instead of model vertically a complex class hierarchy (in the OOP sense) to represent the various game entities from the most generic to the most specific, think about the various attributes (position, strength, etc. ) as a single “component”. Entities are then made as a collection of such components, which are shared (or not!) horizontally.

This is a great rundown about this approach written quite some time ago but it still mostly applies: An exploration of the Entity Component System in Elm · GitHub . Just take into account a few Elm syntactic changes that might appear in the source code.

Of course we have some packages that help with ECS.
https://package.elm-lang.org/packages/harmboschloo/elm-ecs/latest/
https://package.elm-lang.org/packages/justgook/elm-game-logic/latest/
https://package.elm-lang.org/packages/wolfadex/elm-ecs/latest/

I’m sure there are more.

1 Like

I don’t think this is an FP v OOP problem. I’ve been learning Odin in my spare time and I think I’d run into similar data modeling issues despite it not being a pure FP language.

This is a good example of what I mean. The function name suggests I’m working with a ship, but the types suggest I’m working with a generic Body. My hunch is you’d find it easier to work with if you either switched flat record with a more enum like entity type, or duplicated the common fields across every variant of the entity type. The mixing of some fields being in a top record and some being nested tends to make things more difficult for me.

1 Like

While I do like the suggested ECS approach in principle, I dislike its dynamic nature.
In my Elm game, I currently just use one Dict per Entity type and have some shared operations based on extensible records.
Then I can easily statically see which fields a given entity has and I also like the flexibility - if one entity moves a little bit differently than another, no need to hack that into the ECS via extra systems that only target specific entities.
Instead I can just touch the update function for that entity and not use the shared movement function anymore.

So in the example given by OP, I would have

type alias Planet = 
  { mass : Int
  , position : Vector2D
  , velocity : Vector2D
  ...
  , gravity : Float
  }

updatePlanets : Float -> Dict PlanetId Planet -> Dict PlanetId Planet
updatePlanets dt = 
  Dict.update (\_ -> updatePlanet dt)

updatePlanet : Float -> Planet -> Planet
updatePlanet dt planet = 
   -- can chain more operations here
   movement dt planet

-- can be moved to seperate file
movement : Float -> { a | position : Vector2D, velocity : Vector2D } -> { a | position : Vector2D, velocity : Vector2D }
movement dt entity = { entity | position = ... }

Apologies for the formatting, I am on mobile right now :slight_smile:
What do you think about this approach?

2 Likes

Mind pls linking me to your game if it’s open?

Not deployed right now since I’m hosting it on my own hardware and one of the main functionalities (catching Pokemon) is not fully implemented yet.

Elm files are in the frontend folder.

1 Like

OK, a quick update on what’s come up wrt the upside down inheritance model I’ve tried.

  1. It worked really nicely for the simple cases.
  2. It started showing strain when it became evident that the Saucer’s inertialess flight needs to resolve collisions very differently than other bodies. If I have any types that has many variations, the current approach will explode in complexity I suspect.
  3. Projectile modeling surfaced a conflation I had made - That instances and descriptions of game objects are the same thing.

So I now need to go have a deep think about whether I can continue to use the “upside down inheritance” but split descriptions from instances or if doing object inheritance (as opposed to class inheritance) is the way to go here.

I also really like the “idea” of ECS, it seems to be the same idea that Datomic is founded, but it the words “at run time” make me thing I’m going to have trouble of some sort in Elm.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.