Gamedev with Elm Types

screenshot of game

The Elm #gamedev community on Slack has started having monthly game jams. The rules are simple:

  1. Write most of the source in Elm
  2. Make the source publicly available
  3. Focus on code readability

This is a collaborative rather than a competitive event so developers are encouraged to read each other’s code and learn from each other.

February’s game jam set a theme of safety. I decided to write a tower defense game where you play as a tea merchant who has run aground and has to defend against incoming pirates. The game was named “Safe Tea” (yes, this whole game is a bad pun πŸ˜›)

The game can be played online on itch.io, the source is on GitHub.

Leveraging Elm’s type system helped make the experience of building a game much nicer.

Wrap units in custom types

Originally all numbers in the game represented pixels. However, this started changing as the game grew more complex. Sometimes distances were measured in map tiles rather than pixels. Sometimes an (X,Y) pair was relative to the corner of the map, sometimes it was relative to a given pirate. All these numbers were getting confusing.

So I started wrapping them in domain-specific types. This improved both type safety and readability.

Consider the following two signatures:

distance : (Int, Int) -> (Int, Int) -> Int

-- AND

distance : Coordinate.Global -> Coordinate.Global -> Feet

Which one tells the better story?

Composition vs inheritance

There are several in-game entities that need to be rendered visually to the screen: pirates, player ship, bullets. There are a few things in common that all renderable entities need to have.

  1. A position
  2. Width and height (in pixels)
  3. A path to an image (I didn’t have time to figure out spritesheets).

With this information, I can render any entity to the screen. My first idea was to use an extensible record.

type alias Entity a =
  { a
  | position : Coordinate.Global
  , width : Int
  , height : Int
  , imagePath : String
  }

render : Entity a -> Element
render entity =
  -- Render any entity!

It turns out that this approach is like trying to simulate object-oriented inheritance in Elm. It’s a little awkward to deal with and constrains how I’m allowed to model the various game entities. It also means that game entities drag around these extra fields that aren’t relevant until render time. This got particularly tricky when size or image varied based on other state.

So I ditched the extensible record and instead decided to write conversion functions:

-- Entity is now a plain old record

type alias Entity =
  { position : Coordinate.Global
  , width : Int
  , height : Int
  , imagePath : String
  }

render : Entity -> Element
render entity =
  -- Render any entity!

Bullet.toEntity : Bullet -> Entity
Bullet.toEntity bullet =
  -- build an Entity based on the bullet's state

This approach proved particularly flexible. I could now model bullets any way I wanted. They didn’t need to be records, they could be union types or anything else.

Bullets change image and size based on whether they are flying or in one of 3 states of exploding. By deriving the Entity values instead of storing them, I avoided some invalid states.

By taking a derived data approach, I got this flexibility and safety while still being able to have a single render function that could render any entity. Best of both worlds!

Vector math and pipelines

problem solving with vectors

I was trying to figure out how to move a pirate some distance d towards a point that I knew I couldn’t reach this tick. Where does the pirate end up?

The logic ended up looking like this:

  1. The pirate’s current position is the vector A
  2. The place I want to reach eventually is the vector B
  3. The full movement for the pirate is the vector A - B
  4. I can’t do the full movement so I create a new vector C whose direction is the same as A - B but whose magnitude is d
  5. The movement I can do this turn is the vector A + C

If you read these steps and went πŸ˜•πŸ˜ŸπŸ˜°πŸ˜±πŸ™ˆ, you’re not alone. The math was confusing and the code was gnarly.

I was discussing my frustration in the #gamedev channel of the elm-lang Slack. User @mordrax mentioned that it’s nice to have two different coordinate spaces. Global coordinates where (0,0) is the corner of the world and local coordinates where (0,0) is the position of the entity.

A lot of the math, particularly rotations, is much easier if the entity being acted on is at (0,0). Mordrax suggested I create a series of pipeline-able functions to convert between the global and local coordinate spaces as well as for common tranforms.

This ends up looking really nice:

bullet.position
  |> Coordinate.toLocalSpace
  |> Coordinate.moveMilesTowardsPosition distanceInThisInterval
  |> Coordinate.rotateTowardsDestination
  |> Coordinate.toGlobalSpace

It turns out that converting to local space and back into global space is the equivalent of having to do - A and + A in my vector drawing above!

Adding custom types Coordinate.Global and Coordinate.Local to wrap the raw Vector values helps make code more readable and gives me safety of knowing I’m only doing some kinds of operations on certain kinds of positions.

Conclusion

Joining a game jam was both an instructive and rewarding experience. Some big takeaways were:

  1. Use custom types to improve safety and readability of your code.
  2. Ask for help. The #gamedev community is really helpful and supportive.
  3. Draw your problems. Expressing your problems in a different visual medium really helps gain understanding.
  4. Decouple all things game model from where and when they will be represented on the screeen.