Phoenix LiveView is an awesome technology that tries to push Elixir usage for web application way further than the basic Phoenix framework does.
It was presented on April 2019 at ElixirConf EU, and it was immediately a huge hit. It allows to develop real-time, highly interactive web applications, without leaving the realm of Elixir.
You start by defining an initial, server-side rendered
.leex template, which is the extension for LiveView Templates, almost identical to Phoenix Eex Templates. Then, a websocket takes over and becomes the primary mean of communication with the backend: user events like clicks, keypresses, form submissions etc, they all pass through the socket.
All the state is kept within the socket, and lasts for the whole user session. When the state is updated, it is immediately reflected in the templates, and the changes are sent to the client through the websocket as HTML strings. Then LiveView performs DOM diffing between what’s in the actual DOM and the received string, and paints only what actually changed.
This means everything is kept on the backend: templates, state management, render logic, etc. Actually there is a tiny bit of JS you need to write in every project, but it‘s just setup code as we will see later.
Let’s start with the actual tutorial!
The game basics
This tutorial will be about developing a clone of Breakout, called BreakoutEx. Breakout is a game where a ball is moving continuously within a squared, 2D board, bouncing on the walls that surround it. The player controls a paddle that lives in the bottom section within the cage. Usually starting from the center, and going to the top, there are bricks in the cage that the player has to destroy by making the ball touch them. The player must also prevent the ball from reaching the bottom side of the board. The floor is lava!
So what do we need, from a game programming perspective? First of all, a functional representation of a board, with walls, bricks, and the floor. Then, a game loop that performs these steps:
- Be responsive to the paddle being maneuvered by the player
- Make the ball move on each tick
- Collision detection of the ball with bricks, walls, and the floor
With no further ado, let’s dive in the first section.
Step 1: Setup
Install Elixir following the official documentation. Then install Phoenix:
$ mix archive.install hex phx_new 1.4.10
Let’s now create a new Phoenix project, called BreakoutEx:
$ mix phx.new --no-ecto breakoutex
--no-ecto flag is important since we don’t want to deal with databases, for the sake of semplicity.
Open your newly created folder, and edit
mix.exs to add Phoenix LiveView to your deps:
import calls inside
browser pipeline in
Add a new socket endpoint in
Update the endpoint config to include a secret for this endpoint. Open
config/config.exs and add this line:
The setup for the backend is done! Now edit your
assets/package.json to include Phoenix LiveView:
Go and delete the files inside
assets/js/app.js with the following content:
Install both Elixir and JS deps:
$ mix deps.get
$ cd assets && npm i && cd ..
Finally, start the Phoenix application using:
$ mix phx.server
And, that’s it. Open http://localhost:4000/ and you’ll be greeted with the default Phoenix page.
Step 2: The board
The board can be represented using a matrix, where every piece has its own coordinates and type. Every level is composed by:
- Fixed, non removable items (floor/wall/empty pieces)
- Fixed, removable items (bricks)
We could use sigils to describe a level in a concise way:
This is a pretty standard way of describing a level structure using Phoenix LiveView, because it’s easy to parse both for humans and computers. This is the legend to read the map:
Xrepresents a wall
0represents an empty space
Drepresents the floor
yrepresent bricks (and the initial letter of their colors)
This matrix can be fed to a function that translates block types to actual objects with spatial coordinates. Every block lives in the DOM, inside a common ancestor, with an absolute position. For every block we just need
height properties expressed in pixels, and we are ready to draw!
Let’s start with the code, and then we’ll dive in with the explanation. Add this line to
mix deps.get. Now create a new file in
lib/breakoutex_web/live/blocks.ex and paste the following content:
build_board/3 function receives a level shaped like the one I showed above, and returns a list of blocks with spatial coordinates. The code boils down to a double iteration, like normally done with matrices, where the inner loop uses pattern matching on ASCII characters, which correspond to actual block types. Blocks coordinates are created this way:
leftproperty (which is the x-axis coordinate of the top-left vertex): inner loop (x) index + width
topproperty (which is the y-axis coordinate of the top-left vertex): outer loop (y) index + height
This allows to represent blocks using top-left coordinates, which is cool because it’s what most game frameworks use, and it works out of the box with the DOM.
Bricks are a bit more complicated, as they can be removed from the board (thus the
visible property), and they contain two additional spatial coordinates:
bottom. You shouldn’t be too worried by these, since they are pre-computed just for performance sake: in fact,
bottom is equal to top + height, and
right is equal to left + width.
Starting from the list of blocks, we can easily render the board this way. Create a new file in
lib/breakoutex_web/templates/game/index.html.leex and paste this snippet inside:
Variables prefixed by
@ are present inside the socket state, and can be accessed from LiveView Template (
.leex) files. These files have the same syntax as Phoenix Eex Templates, so you should not be worried about them: the different extension is just to help humans differentiate the templates within a project.
The template above takes a
blocks list, which is the board returned by the
build_board/3 function, and creates a
div in the DOM for every block. The LiveView engine tracks changes made to the
blocks list (for example when a brick is removed from the board) and re-renders it accordingly. The new list is then sent down to the client via websocket, and the changes are reflected in the DOM.
Note the usage of
translate3d to express the position of the block in the DOM, instead of declaring the
left CSS properties: the former method leverages the GPU, while the latter forces the CPU to do the computation, which is much slower.
Now that we have an overview of both the structures used to describe the board, as well as the client code to show the board elements in the DOM, we can go along and see how to make everything “animated” and interactive: the game loop.
Step 3: Game Loop Basics
The heart of a LiveView application consists of one or more modules that implement the
Phoenix.LiveView behaviour, which includes two callbacks:
render/1 . The first receives session info and a socket connection, and it’s responsible for managing initial state; the second is triggered every time the socket state changes, and it’s responsible for sending the updated HTML down the line.
The entrypoint for all of this is an HTTP request, which is routed to a LiveView controller, where it’s updated to a stateful, websocket connection. Let’s start with the basic wiring: open
lib/breakoutex_web/router.ex and replace the block
scope "/", BreakoutexWeb do … end with the following lines:
We are just binding a new controller,
GameController, to a route. Now create a new file in
lib/breakoutex_web/controllers/game_controller.ex and put the following snippet inside:
Don’t worry if the compiler says that
BreakoutexWeb.Live.Game is not available, we’ll define it in a moment. This code basically registers a LiveView module as the default request handler and delegates the HTML rendering to the
Game module, which is the one that implements the
Phoenix.LiveView behaviour described above.
Now create a new file in
lib/breakoutex_web/live/game.ex and paste the following code, which contains nothing more than the basic scheduled loop and everything else is a noop:
mount/2 function gets automatically invoked by LiveView. That’s where the initial state is assigned: the blocks composing the board, the basic unit of measure for all the blocks (aka the number of pixels), the tick frequency. The
assign/2 function takes a
Socket struct, which is were the state is being stored, and a map containing the state we want to store.
When the state is updated, the
render/1 function gets automatically invoked, and the pieces of DOM that need to be re-rendered are sent to the client via websocket.
schedule_tick/1 function is where the magic happens: since LiveView implements a GenServer, we can use
Process.send_after/3 to implement a simple infinite loop. We schedule an initial tick, and then every time we handle the game loop, we also schedule another tick in the future. And again, and again. The
game_loop function will be the entrypoint for everything related to ball movement, collision detection, and so on.
Now let’s create a Phoenix View and check what we get by visiting our new created webpage. But first, create a new file in
lib/breakoutex_web/views/game_view.ex and paste this content:
This is just Phoenix boilerplate, and allows to make the
render/1 function above succeed, since it’s calling an existing module, which is an actual
lib/breakoutex_web/templates/layout/app.html.eex and remove the
<header></header> tag. This file contains the default HTML layout for new Phoenix project, and this is where our game container will be rendered. The header takes a lot of space, and we don’t need it right now.
Let’s now add a bit of CSS to our page. Create a new file in
assets/css/game.css and add this content:
The game container has a relative position, while the blocks have an absolute one. So, in order to be able to effectively center the container, we have to add a
left property that starts from
50% and then get half of the board width subtracted. As we saw in the level description above, there are 26 columns, each
20 unit wide. Thus
26 * 20 / 2 yields
260, and that’s why we need to subtract these pixels.
Finally, include these lines in
We’re basically including the file we’ve just created, as well as the default styles for LiveView projects.
Let’s visit http://localhost:4000. The board is rendered correctly, and if you look at the terminal you will notice the text
noop being printed out every 16ms: that’s the game loop.
Step 4: User Input
Now that everything is wired together, we need to handle user input in order to add interactivity to our game.
LiveView make it possible through events, which are specific HTML attributes that need to be put on elements, and that can be intercepted in the backend using pattern matching. There are many events you can bind to, as described in the official docs. For our game, we just want to listen to
keyup events fired at
window level, that is to say globally.
lib/breakoutex_web/templates/game/index.html.leex and wrap the game container like this:
It’s sufficient to declare a
handle_event/3 function within our game module, and the event will be correctly received. Open
lib/breakoutex_web/live/game.ex and add these two functions right before
handle_event/3 function receives three parameters:
- the name of the event we bound to, in this case
- the payload of the specific event
- the socket, which again holds the state of our application
:no_reply tuple is then returned with the new state.
Open http://localhost:4000 and press a couple keys on the keyboard. You should get an output similar to this (if the terminal gets too spammy, remove the
IO.puts("noop") in the
The payload contains everything we need, and a lot more info. We can now intercept what keys are being pressed and released on the browser, and change the application state accordingly. Let’s start from the paddle.
A paddle can be described basically as a brick, but with a speed and a direction. It needs to move based on user input, and stay still when no key is pressed. Let’s try modelling it this way:
- initially, the paddle has a
:stationarystate: it’s still
- then, on a
keydownevent, the state becomes either
right(depending on the actual key that was pressed)
- on every tick of the game loop, the paddle is moved or stays still, based on its state
- whenever a
keyupevent is fired, if it matches the paddle state, then it becomes stationary; otherwise the event is ignored
Let’s start writing some paddle code. Open
lib/breakoutex_web/live/game.ex and add this snippet right below the
@level module attribute:
These constants are useful to place the paddle in a precise space within the board, as well as to describe its size. All the measures are in multiple of the basic unit, which we defined above and represent a fixed value in pixels.
In the same module, go to the
mount/2 function and add the code to represent the initial paddle state to the
I’ve omitted here the creation of the
BreakoutexWeb.Live.Helpers module. To avoid copy-paste, I took the
coordinate/2 function defined inside the
BreakoutexWeb.Live.Blocks module and I moved it into a dedicated module, so it can be re-used here too. The module was created as usual in
The paddle configuration looks very similar to bricks:
length properties share the same semantic with bricks. While we’ve already seen the meaning of the
direction property (it’s the paddle state, and can either be
speed represents the amount of pixels the paddle can move on every tick.
We can now render the paddle in the DOM, just like we did with bricks and other blocks. Open
lib/breakoutex_web/templates/game/index.html.leex and add this snippet inside the game container, just above the
Finally, add a bit of style inside
If you open http://localhost:4000, you can see the pad is there, but it’s missing the necessary wiring to make it move.
Let’s now hook into the
handle_event/3 functions. They should update the paddle
direction property based on the events received from the user. Replace the two functions with these ones inside
We’ve just added the pattern match on
code property inside the payload, and delegated the events handling to other functions. In the same module, add these functions:
We pattern match on specific keys, so to handle them, and ignore the rest. Furthermore, we delegate to other functions the possible move, direction change or stillness of the paddle. In order to make the code compile, add these two module attributes on top of the
Finally, add this snippet to the same module:
Let’s start from
move_paddle/2: we change paddle’s direction only if it’s different from the previous one. On the other hand, in
stop_paddle/2, we reset the direction to
:stationary only if the paddle was already moving in the same direction for which the key was just released. Remember that
move_paddle/2 is invoked on
keydown event, while
Now we can update the paddle state, based on user input. What’s left is to add the move logic in the game loop, and the paddle will start moving.
game_loop/1 function in the same module and replace its contents with these lines:
And right below, add these functions:
On every tick of the game loop, we check the paddle direction. If it’s
right, we update its
right coordinates appropriately. Otherwise, it’s a noop. Since the paddle can only move horizontally,
bottom will always remain fixed (they will be used later on for collision detection).
The coordinate changes consist in one “unit speed” for every tick, that is to say a fixed amount of pixels every time. For movement to the left, there is a
max with the left boundary, aka the wall; for the right movement, there is a
min with the right boundary.
The sign of the operations (
-) is explained by the fact that we assume, as we did for rendering blocks and bricks, that the center of the axes is at the top-left of the board. Thus we decrease the x-coordinate when we move to the left, and increase when we move to the right.
Open http://localhost:4000, and try moving the paddle. It works!
That’s all for part 1! We’ve seen all the most important parts of the LiveView framework, and set up a good foundation for our game. We’re still missing ball movement, collision detection, level wins and losses. Stay tuned for part 2!