Decoupling All the Things

The Elm #gamedev community on Slack has started having monthly game jams. That sounded fun so I built a pirate-themed tower defense game for the February jam.

This was my first attempt at gamedev so I started with the simplest approach. Along the way, I found that I’d created a system where my game world was tightly coupled to environmental factors.

Decouple your in-game coordinate system from the screen

I made an early decision to use canvas to render my game, using the graphics library. At first things were simple. If I wanted to render a pirate 100 pixels from the left of the screen and 200 pixels from the top, I’d give the pirate a position like (100, 200).

This way of modeling things is simple. There is a one-to-one correspondence between the in-game model and the screen. This tight coupling has several drawbacks though.

This approach assumes you’re showing the whole map. You can’t have any portions offscreen, offer any zoom or scroll features. I didn’t end up needing these features but it might have been nice to have an off-screen area of the map to act as a staging area for the next wave of pirates. As it was I didn’t get far enough to really tinker with waves of pirates.

This approach makes coordinate math more difficult. In a traditional Cartesian coordinate system, points exist in one of four quadrants. A rendering system where (0,0) is in the top-left corner and both X and Y increase going right and down respectively doesn’t fit on the traditional Cartesian system. It’s closest to quadrant IV, but with the Y axis flipped. This meant I kept confusing myself over whether or not I needed to negate the Y value in a given situation.

cartesian coordinate system screen coordinate system

This approach forces you to measure in-game distances in pixels. This means you are stuck with a given map and tile sizes. My map tiles were 64x64 pixels and my map was a square 15x15 tiles, making it 960x960 pixels. This is a bit large to put on a screen and I considered making the map tiles a smaller size such as 32x32 pixels. However changing this rendering detail would have broken a lot of my game code. For example, all the starting positions for the pirates and players would now be wrong.

The solution to all of these problems is use a separate coordinate system and distance measure for all the game logic. Then at render time you convert “world” coordinates and distances into “screen” coordinates and pixels. You could even create custom types to distinguish positions in different coordinate spaces.

I figured this lesson out too late to implement those changes everywhere before the deadline so the game still uses a lot of screen positions.

Decouple your game speed from the speed of the render loop

Similar to the previous lesson, this one is a result of coupling game logic to visual decisions.

I originally set my game to tick forward 3 times a second. Every tick, the pirates would move forward by a constant number of pixels. This was helpful as I was programming the movement logic for the pirates but the movement is obviously very jerky. So I sped up the game speed to 30 times a second. Now the pirates moved smoothly but were also much too fast. I had to go and divide that speed constant by 30 to get the desired speed. Ideally the speed of in-game entities remains the same regardless of how fast the game is ticking along.

I was able to do this by subscribing to time elapsed since the last tick. I can then multiply the time elapsed by my rate of speed to get the distance moved in the elapsed period. It ends up looking like this:

move : Time -> Pirate -> Pirate
move diff pirate =
    speedPerSecond =

    distanceInThisInterval =
        (Time.inSeconds diff) * speedPerSecond
    -- actually move the pirate

This system has a bonus advantage: you still get smooth movement even when ticks aren’t 100% regular. I used the animation-frame package to subscribe to the browser’s paint rate. This is usually about 60 frames per second but varies a bit from frame to frame based on a variety of conditions.