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.
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.
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:
| List | Contents | Means |
|---|---|---|
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.
- Three List variables
snakeXandsnakeYhold the column and row of every body part, head first.cellsholds the same squares packed into one number each — see the callout below. - Numbers: dirX and dirYwhich way the snake is travelling.
0, -1is up;1, 0is right. - Numbers: nextX and nextYthe direction the player has asked for. The tick copies these into
dirX/dirY. - Numbers: headX and headYscratch space for the square the head is about to move into.
- Numbers: appleX and appleYwhich square the apple is on.
- Numbers: score and alive
aliveis1while the round is running and0once you've crashed. - Number: bestturn on Persistent for this one so the high score survives closing the app.
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.
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:
- If (True/False)
alive == 1. On false, do nothing — the timer keeps running harmlessly while the game-over screen is up. - Set Variable
dirX→nextX, thendirY→nextY. The turn the player asked for becomes the turn you actually take, once per tick. - Set Variable
headX→snakeX[0] + dirX, thenheadY→snakeY[0] + dirY. That's the square the head is moving into. - 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.
- If (True/False)
headX == appleX && headY == appleY. - True — you atea Set Variable putting
scoretoscore + 1, and then nothing else. No tail removal. The snake is now one square longer. - 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. - Both branches then meetat the crash test in step 5.
Step 5: crashing, and moving in
- If (True/False)
array_contains('cells', headY*100 + headX)— is the new square part of the body? True goes to the crash blocks. - Insert into Arraythree of them, index
0, one per list. Values through the Expression solver:headY*100 + headXforcells,headXforsnakeX,headYforsnakeY. Index 0 means "put it on the front" — this is the new head. - If (True/False)
score > best→ Set Variablebesttoscore. - If (True/False)
headX == appleX && headY == appleYagain → Broadcast Messageeat, so the apple moves somewhere new. - Broadcast Message
draw. 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:
- On Message
draw. - If (True/False)
array_length('cells') > 3— is the snake long enough to have a fourth part? - True → Change Position/SizeX through the Expression solver:
-144 + snakeX[3]*32. Y:192 - snakeY[3]*32. - False → Change Position/SizeX
3000, Y3000. 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.
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:
- When Object Touched
- If (True/False)
dirY == 0— only allow an up-or-down turn while you're travelling sideways. - Set Variable
nextXto0, thennextYto-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.
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
| Block | What it did here |
|---|---|
| Repeat (Timer) | The heartbeat — one grid step every 0.22 seconds |
| Insert into Array | Put the new head square on the front of the lists |
| Remove from Array | Took the tail tip off the back — skipped when you eat |
| Clear Array / Add to Array | Dealt 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/Size | Moved every part onto its square, or off screen |
| Broadcast Message / On Message | One 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
| Symptom | Cause |
|---|---|
| The apple is never eaten, walls do nothing | You'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 turns | You test for the crash before removing the tail. Remove first |
| Body parts all sit on top of each other | They 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 back | You hid it with Set Active, which froze its script too. Move it off screen instead |
| The score never changes on screen | Set Text's expression box has a sum in it. It only accepts a single variable name |
| The snake instantly dies when you turn | Missing direction guards, so it reversed into its own neck |
| Nothing happens after your Repeat block | Nothing can follow a Repeat. Move those blocks into the loop or into a separate chain |
| The apple lands under the snake | No re-roll loop, or the loop runs before the new head is added |
| The snake keeps growing past your last part | End 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.
In Max2D: Projects → Import 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.