A reptile steals the scene

We cannot call our game a snake game without a snake, though, so let's fill that gap. We'll introduce some changes to our init/2 function to prepare our game state to maintain relevant information about every 'game object' (the snake and the pellet) in a way that we can later push them to a single function that will render them to the screen according to their specific rules. This is our updated function:

We use the ViewPort.info/1 function to find out the size, in pixels, of our current viewport. Since our play field will be a grid, it's useful to know how many tiles we can fit in each dimension for two reasons: we can determine the snake's initial position such that it always starts in the center of the grid, and we can make the snake wrap around the screen later on.

Notice we're storing our game objects inside a key named objects in the game state. We store three important pieces of data about the snake object:

A list of ordered pairs that describe the snake's full body . Each pair corresponds to a cell in the grid that the snake is currently occupying.

. Each pair corresponds to a cell in the grid that the snake is currently occupying. The snake's current size in cells. We'll use it soon to determine whether the body should grow at any given step.

in cells. We'll use it soon to determine whether the should grow at any given step. The direction in which the snake is currently heading. It is an ordered pair that tells us how many cells the snake head's position is shifting in each direction, per update. For instance, the starting value of {1, 0} means that it will jump 1 cell to the right and 0 cells down at a time.

The draw_game_objects/2 function will apply the transforms from all objects in the state to the scene graph. Its implementation follows:

The main concept here is that each object will have its draw_object/3 entry describing how it should be rendered, starting with :snake . To bring the game's protagonist to life, all we need are a few cells painted lime. For this purpose, we create a nice draw_tile/4 function to help us fill a cell at coordinate (x,y) whenever we need. Don't forget to update our imports list as we're now using rrect/3 from Scenic.Primitives and, while we're at it, take a moment to insert the new tile_radius module attribute that defines how rounded our rectangles will be:

import Scenic.Primitives, only: [rrect: 3, text: 3] @tile_radius 8

Boot up the game once more, lo and behold:

It lives! Sorta?

Well, it certainly is a rounded rectangle. But it still needs to move and grow in order to resemble a virtual snake. However, right now, our game screen is only updated exactly once: when the scene starts. Before we get to gameplay mechanics, we need to set up a mechanism to periodically update whatever has to be updated and re-render the game screen. Fortunately, Erlang's :timer provides a method that is simple enough for our purposes, one that fits in our init/2 :

This will make sure our scene receives a :frame message every @frame_ms milliseconds, so don't forget to define this module attribute. Some value like 192 is okay for starters. Lower values will speed up the game's pace.

Now that we're receiving a periodic message, we should be capable of handling it:

Even though it doesn't look too exciting yet, this is the essence of our game loop! It takes the current state as a parameter, utilizes move_snake/1 (which we'll implement next) to update it, and runs the same draw pipeline as init/2 resulting in a new graph push, which is what triggers the rendering process. Now, to get the snake to move:

Our movement logic boils down to this:

Take the snake's current head position; Create a new head by shifting a copy of the old one in the snake's current direction; Limit the snake's size to the current known size.

Just like nature itself… right? As you can see, these operations can be expressed without much effort in Elixir's [head | tail] notation for lists, as is commonly found in functional programming languages. In fact, what better way to work with a head and a tail than to program a snake?

To help us with the shifting part, move/3 adds a {vec_x, vec_y} tuple (the displacement) to a {pos_x, pos_y} tuple (the original position). Wrapping around the screen is done with the modulo operator in the form of rem/2 .

Now, when you run the game once more, you should see this:

A free-range snake! Just look it him go!

Our snake has finally learned how to slither! Such unbounded excitement, it now knows no limits. It also won't listen to orders, so if you want it to follow another direction, tough luck. We can remedy this, however, by handling keyboard events.