Pushing Phoenix LiveView

Last week, Alembic ran a full day Intro to Elixir workshop at LambdaJam in Melbourne. We assumed our students had no prior Elixir knowledge and took them through the basics, using the iex REPL which towards the end of the day culminated in implementing some simple Phoenix LiveView apps.

As part of the course material we had built an Asteroids-style vector physics game using LiveView (with some features deliberately absent) so that the students could have a go at grokking a more complex application and see if they could add the missing features. It was great to see some of the attendees get super excited; for example we had one attendee try to add sound effects by rendering <audio> elements to play sound effects. Someone else tried to reduce the size of the rendered SVG by using lower precision vector representations.

In summary we had a lot of fun!

Introducing Blast!

Blast is the game that we built for the workshop, although it's present state is slightly more polished (but still utterly unpolished!) than on workshop day. (What can I say? Working on this after hours has been fun).

Blast is a multiplayer Asteroids-style game. The backend is a Phoenix LiveView app which renders an SVG representation of the game arena at 60FPS (YMMV). It's a testament to the power of LiveView that this can be rendered in real-time https://alembic.com.au/blog/2019-05-30-pushing-phoenix-liveviewacross the internet. It's currently deployed on a free tier instance at Gigalixir and can be found here.


How it hangs together

The heart of the game is the GameState module. This is a struct representing the entire world with all active players, sound effects and projectiles, plus functions for updating it. This module is purely functional with no side-effects.

Wrapping that is a GenServer called GameServer. The GameServer manages a single GameState and updates the state of the world every 16 milliseconds. At the end of every 16ms server frame it uses Phoenix PubSub to post the state of the world to all subscribers. GameServer is therefore not purely functional because it actually performs I/O and manages timing.

A LiveView implementation called GameLive subscribes to the GameState messages sent by the GameServer and renders an SVG. GameLive is also a GenServer and there is one instance of GameLive per player connected to the game (Blast currently supports up to 4 players, which is a hard coded limit and could probably suport more).

All of this orchestration is capable of delivering 60FPS (ish) to each person connected to the game although it gets janky across the internet - running on a local network is much faster.

You probably wouldn't want to write a game like this

Modern multiplayer network games use various latency compensation techniques and tend to prefer UDP over TCP. The world simulation will run on both the client and the server so that the client can smooth out gameplay in adverse network conditions when packet loss or latency is an issue.

You don't get that with LiveView - the frontend is really just a simple client, meaning that the client only updates the display when told to do so by the server. It uses a websocket over HTTP - which is TCP underneath. That means dropped packets will cause a pause in rendering while the protocol stack recovers and the packets are re-transmitted in order.

But overall, it's still a testament to LiveView that this kind of setup could produce a playable game at all.

Sound effect generation

This was a fun one. LiveView's only means of interacting with the client is via DOM mutations. So getting audio to work was a challenge. At the workshop one of the students tried to get sound effects working via rendering <audio> tags. This produced mix results - Chrome in particular did not like this and produced janky audio and a huge CPU spike which caused the frame rate to dramatically slow down.

In the end we went for rendering <span data-src="sound-effect.wav"/> elements in a hidden div and we added some JavaScript to the client to watch for DOM mutations on the hidden element. When it sees a new element we used the Web Audio API to play the sound effect. So yeah, we cheated a little bit and used some JavaScript.

Hacking on Blast

If you're interested in checking out the source then feel free to check out the code here. Fork the repo, and PRs are welcome! There are a bunch of feature ideas documented in the README.

The code is rough around the edges; constructive feedback and PRs are welcome.