Chapter 2: The Snake
July 14, 2025 ยท View on GitHub
In the previous chapter, we learned about the Game struct as the central brain of our Snake game, holding all the important Game State, and how it interacts with the Ebitengine Game Engine through the Update and Draw methods. One of the most crucial pieces of game state managed by our Game struct is, of course, the Snake itself!
The Snake is the player-controlled character. It's not just a single point on the screen; it's a creature made of connected segments that grows and moves. How do we represent something like this in our game state? This is where our concept of "The Snake" comes in.
What is the Snake?
Think of the snake as a train. The first car is the engine (the head), and the cars behind it are the body segments. As the train moves, each car follows the one in front of it. When the train eats something, a new car is added to the end. If the engine hits a wall or one of the other cars, the game stops.
In our game code, we represent this "train" using a list (specifically, a Go slice) of grid coordinates. Remember from Chapter 1 that our game world is like a grid? Each point on this grid can be identified by its x and y coordinates.
Let's look at how the snake is stored in our Game struct:
type Game struct {
snake []Point // Where the snake's body parts are (a list of coordinates)
direction Point // Which way the snake is moving (e.g., {1, 0} for right)
// ... other game state
}
// Point represents a coordinate on the grid
type Point struct {
x int
y int
}
snake []Point: This is a slice ([]) ofPointstructs. EachPointin this slice represents the grid cell occupied by one segment of the snake. The firstPointin the slice (g.snake[0]) is always the head of the snake. The rest are the body segments, in order from head to tail.direction Point: ThisPointdoesn't represent a position, but rather a change in position. It tells us which way the snake is currently trying to move. For example,{1, 0}means move 1 step right and 0 steps up/down,{0, -1}means move 0 steps left/right and 1 step up (sinceyincreases downwards).
How the Snake Moves
The snake moves one grid cell at a time. In our game, this movement doesn't happen every frame (every call to Update). It happens only after a certain amount of time has passed, controlled by the gameSpeed variable and tracked using lastUpdate (as seen in Chapter 1). This gives the classic, blocky Snake movement.
When it's time to move, the game needs to calculate the new position of the head based on the current direction, and then shift the rest of the body segments to follow the head's previous position.
Here's the core function that handles moving the snake:
func (g *Game) updateSnake(snake *[]Point, direction Point) {
// 1. Get the current head position
head := (*snake)[0]
// 2. Calculate the new head position based on direction
newHead := Point{head.x + direction.x, head.y + direction.y}
// 3. Create a new slice: new head followed by the OLD body (excluding the last segment)
*snake = append([]Point{newHead}, (*snake)[:len(*snake)-1]...)
// The old tail segment is simply dropped because the rest of the body shifted
}
Let's trace this with a simple example:
Imagine the snake is at [{5, 5}, {4, 5}, {3, 5}] (head at 5,5, body at 4,5, tail at 3,5) and the direction is {1, 0} (moving right).
headis{5, 5}.newHeadis{5 + 1, 5 + 0}which is{6, 5}.- The slice
(*snake)[:len(*snake)-1]is the old body without the tail:[{4, 5}, {3, 5}]. append([]Point{newHead}, ...)creates a new slice:[{6, 5}, {4, 5}, {3, 5}].- The
*snake = ...line replaces the old snake slice with this new one.
The snake has successfully moved one step to the right! The segment that was at {4,5} is now the first body segment, the segment at {3,5} is now the second, and the segment that was the tail at {3,5} is gone (it was effectively replaced by the segment that was at {4,5}).
This updateSnake function is called from the Game.Update() method when time.Since(g.lastUpdate) is greater than or equal to gameSpeed, making the snake move at a controlled pace.
Growing the Snake
The snake grows when it eats food. How does growSnake() work? Instead of dropping the tail segment like updateSnake does, growSnake keeps the tail segment in place while still adding the new head.
func (g *Game) growSnake() {
// Get the current tail position
tail := g.snake[len(g.snake)-1]
// Add the tail back to the end of the snake slice
g.snake = append(g.snake, tail)
// The next time updateSnake is called,
// the new head will be added, and the *original* tail segment
// will NOT be removed, effectively making the snake one segment longer.
}
This function is called from Game.Update() after checkFoodCollision() determines the snake's head is in the same grid cell as the food. After growSnake is called, updateSnake is called immediately in the same Update frame, moving the snake forward and adding the new head while keeping the old tail, thus growing the snake.
Checking for Collisions
A key part of the snake's behavior is colliding with things. The game ends if the snake hits a wall or its own body.
func (g *Game) checkCollision(point Point) bool {
// Check collision with the snake's own body (excluding the head)
for _, body := range g.snake[1:] { // Start from the second element (index 1)
if point == body {
return true // Collision with body
}
}
// Check collision with walls (grid boundaries)
if point.x < 0 || point.x >= gridWidth || point.y < 0 || point.y >= gridHeight {
return true // Collision with wall
}
return false // No collision
}
This checkCollision function is called in Game.Update() right after the snake's head has moved to its newHead position. It checks if this newHead position collides with any body segment (g.snake[1:]) or the grid boundaries. If checkCollision returns true, g.gameOver is set to true, stopping the game logic in subsequent Update calls.
Drawing the Snake
In the Game.Draw() method, we need to draw each segment of the snake onto the screen. We loop through the g.snake slice and draw a colored square for each Point.
func (g *Game) drawSnake(screen *ebiten.Image) {
for i, point := range g.snake {
if i == 0 {
// Draw the head in red
vector.DrawFilledRect(screen, float32(point.x*gridSize), float32(point.y*gridSize), gridSize, gridSize, RED, true)
} else {
// Draw body segments (with fading color)
fadingFactor := math.Abs(1.0 - float64(i)/float64(len(g.snake)))
faded := color.RGBA{
R: uint8(float64(tailcolor.R) * fadingFactor),
G: uint8(float64(tailcolor.G) * fadingFactor),
B: uint8(float64(tailcolor.B) * fadingFactor),
A: tailcolor.A,
}
vector.DrawFilledRect(screen, float32(point.x*gridSize), float32(point.y*gridSize), gridSize, gridSize, faded, false)
}
}
}
This function iterates through the g.snake slice. For the first element (the head, i == 0), it draws a red square. For the rest of the elements (the body, i > 0), it calculates a fadingFactor based on the segment's position in the body (closer to the tail is darker) and draws a square with a faded color. This is called by Game.Draw(screen *ebiten.Image) every frame to make the snake visible.
The Snake's Role in the Game Loop
Let's visualize how the snake fits into the main Game Loop (Update and Draw cycle) we discussed in Chapter 1.
sequenceDiagram
participant E as Ebitengine Engine
participant G as Our Game (Game struct)
participant SnakeD as Snake Data (g.snake, g.direction)
E->>G: Call Update()
G->>SnakeD: Check g.paused, g.gameOver
alt Game is Active
G->>G: Handle input (change g.direction)
G->>SnakeD: Check time.Since(g.lastUpdate) vs gameSpeed
alt Time to Move
G->>SnakeD: Call updateSnake()
SnakeD-->>G: Update snake position
G->>G: Check g.checkCollision(newHead)
alt Collision Occurs
G->>G: Set g.gameOver = true
G->>G: Play game over sound
else No Collision
G->>G: Check g.checkFoodCollision()
alt Food Eaten
G->>SnakeD: Call growSnake()
SnakeD-->>G: Snake grows
G->>G: Play food sound, increase score
else No Food Eaten
G->>G: Maybe generate new food?
end
end
G->>SnakeD: Update g.lastUpdate = time.Now()
end
G->>G: Update other things (particles, buttons)
end
E->>G: Call Draw(screen)
G->>SnakeD: Read snake positions (g.snake)
G->>G: Call drawSnake(screen)
Note over G: Draw each segment<br/>onto the screen
G->>G: Draw other things (food, score, text)
E-->>E: Display screen to player
Note over E: Repeat this cycle
This diagram shows how Game.Update uses the snake and direction data to determine the snake's new position, check for collisions, handle growth, and update the game state. Game.Draw then uses the updated snake data to render the snake on the screen.
In Summary
The Snake, representing the player, is a core part of the game state held within the Game struct. It's represented by a list of grid coordinates ([]Point), with the first element being the head. The snake's behavior (moving, growing, colliding) is implemented through functions like updateSnake, growSnake, and checkCollision, which are called by the main Game.Update method at the appropriate times. The Game.Draw method then makes the snake visible using the drawSnake function. Understanding how the snake's data is stored and manipulated is crucial to seeing how the game logic works.
Now that we understand the main character, let's look at what it interacts with!
References: [1]