Guide

How to make a Snake game

Everybody's first instinct with Snake is to draw a snake, drop an apple in front of it, and wire up a collision. It's the obvious build, and it's the reason most Snake attempts stall. The head is easy. The tail is the hard part — and collision detection turns out to be the wrong tool for the whole job. This guide builds Snake the way it was built originally: as a grid and a list of squares, with no collision detection anywhere in the project.

Needs the arrays update This guide uses List variables and the Array blocks. If you don't see Add to Array in the Add Block sheet under Actions, update Max2D first.

Start here, because this catches everyone

Before any of the build: if you've already tried Snake and your apple refuses to be eaten, this is almost certainly why.

Two Kinematic objects never touch The physics engine only registers a contact when at least one of the two objects is Dynamic. If your snake is Kinematic and your apple is Kinematic — or your wall is Static — they pass straight through each other and On Collision Enter and Is Colliding With never fire once. Nothing errors. Nothing turns red. The blocks just sit there being false forever.

The quick fix anywhere in Max2D is to set one of the two to Dynamic and its Gravity Scale to 0 so it doesn't fall. But for a grid game there's a better answer, which is the rest of this guide: don't use physics at all.

The idea, in one picture

Stop thinking about a snake and start thinking about a chessboard. The play area is 10 squares across and 13 down. Every square has a column number 0–9 and a row number 0–12. The snake is nothing but a list of the squares it's sitting on, head first:

ListContentsMeans
snakeX[5, 5, 5]all three parts are in column 5
snakeY[6, 7, 8]head on row 6, then 7, then 8 below it

Moving is two operations on those lists: put the new square on the front, take the last square off the back. Do that five times a second and you have a snake crawling across the board. Everything else in the game — eating, growing, dying, drawing — is a small question asked about those two lists.

Step 1: the variables

Open the Variables panel and make all of these before you build a single block. Blocks can only point at variables that already exist.

  1. Three List variablessnakeX and snakeY hold the column and row of every body part, head first. cells holds the same squares packed into one number each — see the callout below.
  2. Numbers: dirX and dirYwhich way the snake is travelling. 0, -1 is up; 1, 0 is right.
  3. Numbers: nextX and nextYthe direction the player has asked for. The tick copies these into dirX/dirY.
  4. Numbers: headX and headYscratch space for the square the head is about to move into.
  5. Numbers: appleX and appleYwhich square the apple is on.
  6. Numbers: score and alivealive is 1 while the round is running and 0 once you've crashed.
  7. Number: bestturn on Persistent for this one so the high score survives closing the app.
Why a third list To find out whether the head just hit the body you'd have to check both the column and the row against every single part — a loop, every tick. Instead, keep a third list where each square is stored as one number: row × 100 + column. Square (5, 6) becomes 605. Now the entire self-collision test is one block: array_contains('cells', headY*100 + headX). Every time you add or remove a square from snakeX and snakeY, do the same to cells.

Step 2: the board and the body parts

Add one Sprite object for the board background, sized to your grid — 10 × 13 squares of 32 pixels is 320 × 416.

Then add your body parts. One object per part, each with its own name: part0 for the head, then part1, part2, all the way up. Make as many as the longest snake you'll allow — the example project makes 90. Give them all the same body picture except part0, which gets a head with eyes.

Every one of them needs a Rigid Body set to Static with Is Sensor ticked, and a Box Collider. Not because anything collides — nothing in this game does — but because the Change Position/Size block can only move an object that has a body. Static sensors are the cheapest kind: they can never take part in a contact.

Park them all off screen to start with, at something like x 3000, y 3000. They'll move themselves into place the moment the game starts.

Why park them instead of hiding them Set Active false looks like the right way to hide the unused parts, and it will break the game. Set Active also freezes that object's own scripts — so a part that switches itself off has nothing left running to notice it's needed again. Moving it far off screen keeps its script alive; the camera simply never draws it. Any object that has to disappear and come back under its own control needs to move away, not switch off.

Step 3: the tick

Add one Empty object called manager. This is where the game actually lives. Give it a When Object Loads block leading into a Repeat (Timer) set to 0.22 seconds.

Repeat (Timer) has two outputs. The side one fires every 0.22 seconds forever — that's your tick. The normal one runs once, immediately, so hang your setup chain off that: Clear Array on all three lists, then Add to Array three times each to deal the starting snake, then Set Variable blocks to put dirX/dirY at 0/-1, score to 0 and alive to 1.

Now the tick chain itself. In order:

  1. If (True/False)alive == 1. On false, do nothing — the timer keeps running harmlessly while the game-over screen is up.
  2. Set VariabledirXnextX, then dirYnextY. The turn the player asked for becomes the turn you actually take, once per tick.
  3. Set VariableheadXsnakeX[0] + dirX, then headYsnakeY[0] + dirY. That's the square the head is moving into.
  4. If (True/False)headX < 0 || headX > 9 || headY < 0 || headY > 12 — did we leave the board? True goes to the crash blocks in step 5.

Step 4: eating, and how growing actually works

Here's the part that surprises people. The snake never grows. Every tick it gains a square at the front and loses one at the back, so its length doesn't change. On the tick where it eats, you simply skip the losing-one-at-the-back step. That's the whole mechanic.

  1. If (True/False)headX == appleX && headY == appleY.
  2. True — you atea Set Variable putting score to score + 1, and then nothing else. No tail removal. The snake is now one square longer.
  3. False — normal movethree Remove from Array blocks, one for each list, with the index set through the Expression solver to array_length('cells') - 1. That's the last square: the tip of the tail.
  4. Both branches then meetat the crash test in step 5.
Order matters here, and it's the difference between a fair game and an annoying one Drop the tail before you test for a crash, not after. When the snake is moving in a tight coil the head often steps into the square its own tail tip is leaving that same instant. Test first and you die for no visible reason; drop first and the square is already free, exactly like the arcade original. It's one block moved, and it's the single most common complaint about home-made Snake.

Step 5: crashing, and moving in

  1. If (True/False)array_contains('cells', headY*100 + headX) — is the new square part of the body? True goes to the crash blocks.
  2. Insert into Arraythree of them, index 0, one per list. Values through the Expression solver: headY*100 + headX for cells, headX for snakeX, headY for snakeY. Index 0 means "put it on the front" — this is the new head.
  3. If (True/False)score > bestSet Variable best to score.
  4. If (True/False)headX == appleX && headY == appleY again → Broadcast Message eat, so the apple moves somewhere new.
  5. Broadcast Messagedraw. Last block in the tick.

The crash blocks are two blocks: Set Variable alive to 0, then Broadcast Message gameover. A Text object listening on On Message for gameover puts GAME OVER on the screen.

Step 6: drawing the snake

Every body part has the same two-block script, and the only thing that changes between them is one number. This is part3:

  1. On Messagedraw.
  2. If (True/False)array_length('cells') > 3 — is the snake long enough to have a fourth part?
  3. True → Change Position/SizeX through the Expression solver: -144 + snakeX[3]*32. Y: 192 - snakeY[3]*32.
  4. False → Change Position/SizeX 3000, Y 3000. Park it; the snake isn't this long yet.

part0 tests > 0 and reads snakeX[0], part1 tests > 1 and reads [1], and so on. Build one, copy it, change two numbers. The -144 and 192 are just where square (0,0) sits on your screen; the 32 is your square size.

The apple is the same idea with one extra chain. On On Message draw it moves itself to -144 + appleX*32, 192 - appleY*32. On On Message eat it picks a new home: Set Variable appleX to math_random(10) and appleY to math_random(13), then a Repeat (Times) of 12 containing an If (True/False) on array_contains('cells', appleY*100 + appleX) that rolls again if it landed on the snake.

Nothing can follow a Repeat Repeat (Times) runs the chain hanging off it however many times you asked, and then that chain is finished — anything you attach after the Repeat block never runs. So a Repeat has to be the last thing in its chain. That's why the apple's re-roll loop lives in its own On Message chain instead of inline in the tick, and why the manager broadcasts eat before draw rather than doing the re-roll itself.

Step 7: the controls

Four Sprite objects with arrow pictures and Box Colliders, laid out as a D-pad. The up button:

  1. When Object Touched
  2. If (True/False)dirY == 0 — only allow an up-or-down turn while you're travelling sideways.
  3. Set VariablenextX to 0, then nextY to -1.

Down is the same guard with nextY of 1. Left and right guard on dirX == 0 instead, and set nextX to -1 or 1.

Those guards are what stop you turning straight back into your own neck, which would be an instant crash. And because the buttons write to nextX/nextY while the guard reads dirX/dirY, even two very fast taps between one tick and the next can only ever leave you facing a legal direction.

Step 8: the score on screen

Add a Text object, give it an On Message block for draw, and a Set Text block.

Set Text's expression box is a variable name, not a sum This one is easy to get wrong. The expression box on Set Text doesn't work like the Expression solver everywhere else — it takes the name of one variable and shows you its value. Type score and you get the score. Type something like 'SCORE ' + text(score) and you get nothing at all: the label silently never updates and there's no error to tell you why.

So put the number in one Text object bound to score, and the word SCORE in a second Text object next to it that never changes. Same for the high score and best.

Two modes for free

Duplicate the scene and change one thing. In the classic scene, leaving the board is death. In a "free" scene, replace that single wall test with four small ones that wrap you round instead: headX < 0 → set headX to 9; headX > 9 → set it to 0; the same for headY with 12. Everything else is identical. Two genuinely different games out of one build.

The blocks you used

BlockWhat it did here
Repeat (Timer)The heartbeat — one grid step every 0.22 seconds
Insert into ArrayPut the new head square on the front of the lists
Remove from ArrayTook the tail tip off the back — skipped when you eat
Clear Array / Add to ArrayDealt the starting snake, at load and on restart
array_contains('cells', …)The entire self-collision test, in one expression
array_length('cells')How long the snake is — which parts to draw, where the tail is
snakeX[3]Read one body part's square straight out of the list
Change Position/SizeMoved every part onto its square, or off screen
Broadcast Message / On MessageOne tick tells 90 parts, the apple and the score to update
math_random(10)Picked the apple's new square

When it doesn't behave

SymptomCause
The apple is never eaten, walls do nothingYou're using collision between two objects that aren't Dynamic. Compare square numbers instead, or make one of them Dynamic with Gravity Scale 0
The snake dies for no reason in tight turnsYou test for the crash before removing the tail. Remove first
Body parts all sit on top of each otherThey share an object name, or they all read the same list slot. Each part needs its own name and its own index
A part vanishes and never comes backYou hid it with Set Active, which froze its script too. Move it off screen instead
The score never changes on screenSet Text's expression box has a sum in it. It only accepts a single variable name
The snake instantly dies when you turnMissing direction guards, so it reversed into its own neck
Nothing happens after your Repeat blockNothing can follow a Repeat. Move those blocks into the loop or into a separate chain
The apple lands under the snakeNo re-roll loop, or the loop runs before the new head is added
The snake keeps growing past your last partEnd the round when array_length('cells') reaches the number of parts you built — that's a win

Try it: Snake

The example project is the finished game — two modes, 90 body parts, a D-pad and a saved high score. Open manager to read the tick from top to bottom; every chain has a note next to it explaining what it does and why. Open any part object to see how little each one has to know.

Change the board size by editing the numbers in the wall test and the drawing expressions. Change the speed with the one number on the Repeat (Timer). Add a second apple by copying the apple object and giving it its own pair of variables.

Get the example project

Download Snake.zip (23 KB)

In Max2D: ProjectsImport Project → pick the zip. Needs a version of the app with the Array blocks — if Add to Array isn't in your Add Block sheet, update first.

Frequently asked questions

How do I make a Snake game in Max2D?

Treat the screen as a grid and keep the snake in List variables — one list of columns, one of rows. A Repeat (Timer) puts a new square on the front every fifth of a second and takes the last one off the back. Each body part is its own object that reads its own slot and moves itself there.

Why doesn't my snake detect the apple?

Almost always because both objects are Kinematic or Static. The physics engine only makes a contact when at least one of the two is Dynamic, so nothing ever touches and Is Colliding With stays false forever with no error. Set one to Dynamic with Gravity Scale 0 — or drop physics and compare square numbers, like this guide does.

How does the snake grow?

It doesn't. It adds a square at the front and removes one at the back every tick, so the length is constant. On the tick where it eats, you skip the removal. That skipped step is the growing.

How do I stop it turning back into itself?

Guard each button with an If (True/False): up and down only work while dirY == 0, left and right only while dirX == 0. Write the answer into nextX/nextY and let the tick copy it across, so fast taps can't sneak a reversal through.

How many body parts do I need?

As many as the longest snake you'll allow — the example makes 90. Build them up front, park the spares off screen, and end the round as a win when the snake reaches the last one. That's tidier than growing past them and dragging an invisible tail that still kills you.

A grid and two lists. That's Snake.

Build it tonight on your phone. Max2D is free on Google Play.

Free onGoogle Play