The Elm #gamedev
community on Slack has started having monthly game
jams. The rules are simple:
- Write most of the source in Elm
- Make the source publicly available
- 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.
- A position
- Width and height (in pixels)
- 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
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:
- The pirate’s current position is the vector
A
- The place I want to reach eventually is the vector
B
- The full movement for the pirate is the vector
A - B
- I can’t do the full movement so I create a new vector
C
whose direction is the same asA - B
but whose magnitude isd
- 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:
- Use custom types to improve safety and readability of your code.
- Ask for help. The
#gamedev
community is really helpful and supportive. - Draw your problems. Expressing your problems in a different visual medium really helps gain understanding.
- Decouple all things game model from where and when they will be represented on the screeen.