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

The --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:

Add these import calls inside lib/breakoutex_web.ex :

Edit the browser pipeline in lib/breakoutex_web/router.ex :

Add a new socket endpoint in lib/breakoutex_web/endpoint.ex :

Update the endpoint config to include a secret for this endpoint. Open config/config.exs and add this line:

Please note that this is just the default config. You must use a secure, secret salt in production!

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 . Create 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:

X represents a wall

represents a wall 0 represents an empty space

represents an empty space D represents the floor

represents the floor r / b / g / o / p / y represent 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 top , left , width and 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 deps in mix.exs :

And run mix deps.get . Now create a new file in lib/breakoutex_web/live/blocks.ex and paste the following content:

The 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:

left property (which is the x-axis coordinate of the top-left vertex): inner loop (x) index + width

property (which is the x-axis coordinate of the top-left vertex): inner loop (x) index + width top property (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 id and visible property), and they contain two additional spatial coordinates: right and 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 top and 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: mount/2 and 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:

The 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.

The schedule_tick/1 function is where the magic happens: since LiveView implements a GenServer, we can use handle_info/2 and 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 view module.

Now open 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 assets/css/app.css , below @import phoenix.css :

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 keydown / keyup events fired at window level, that is to say globally.

Open 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 game_loop/1 :

The handle_event/3 function receives three parameters:

the name of the event we bound to, in this case keydown and keyup

and the payload of the specific event

the socket, which again holds the state of our application

A :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 game_loop/1 function):

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 :stationary state: it’s still

state: it’s still then, on a keydown event, the state becomes either left or right (depending on the actual key that was pressed)

event, the state becomes either or (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 keyup event 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 state variable:

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 lib/breakoutex_web/live/helpers.ex .

The paddle configuration looks very similar to bricks: width , height , top , left , bottom , right and 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 :stationary , :left or :right ), the 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 @blocks render:

Finally, add a bit of style inside assets/css/game.css :

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 lib/breakoutex_web/live/game.ex :

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 lib/breakoutex_web/live/game.ex module:

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 stop_paddle/2 on keyup .

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.

Enter the 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 left or right , we update its left and right coordinates appropriately. Otherwise, it’s a noop. Since the paddle can only move horizontally, top and 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 ( + or - ) 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!