How do I chain the movement of a snake's body?
I want to implement a snake game. The snake meanders through the playground. Every time when the snake eats some food, the length of the snake increase by one element. The elements of the snakes body follow its head like a chain.
snake_x, snake_y = WIDTH//2, HEIGHT//2
body = []
move_x, move_y = (1, 0)
food_x, food_y = new_food(body)
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT: move_x, move_y = (-1, 0)
elif event.key == pygame.K_RIGHT: move_x, move_y = (1, 0)
elif event.key == pygame.K_UP: move_x, move_y = (0, -1)
elif event.key == pygame.K_DOWN: move_x, move_y = (0, 1)
snake_x = (snake_x + move_x) % WIDTH
snake_y = (snake_y + move_y) % HEIGHT
if snake_x == food_x and snake_y == food_y:
food_x, food_y = new_food(body)
body.append((snake_x, snake_x))
# [...]
How do I accomplish, that the body parts follow the snake's head on its path, when the snake's head moves ahead?
Solution 1:
In general you have to distinguish between 2 different types of snake. In the first case, the snake moves in a grid and every time when the snake moves, it strides ahead one field in the grid. In the other type, the snakes position is not in a raster and not snapped on the fields of the grid, the position is free and the snake slides smoothly through the fields.
In former each element of the body is snapped to the fields of the grid, as the head is. The other is more trick, because the position of a body element depends on the size of the element and the dynamic, previous positions of the snakes head.
First the snake, which is snapped to a grid.
The elements of the snake can be stored in a list of tuples. Each tuple contains the column and row of the snakes element in the grid. The changes to the items in the list directly follow the movement of the snake. If the snake moves, a the new position is add to the head of the list and the tail of the list is removed.
For instance we have a snake with the following elements:
body = [(3, 3), (3, 4), (4, 4), (5, 4), (6, 4)]
When the snakes head moves form (3, 3)
to (3, 2
), then the new head position is add to the head of the list (body.insert(0, (3, 2)
):
body = [(3, 2), (3, 3), (3, 4), (4, 4), (5, 4), (6, 4)]
Finally the tail of the ist is removed (del body[-1]
):
body = [(3, 2), (3, 3), (3, 4), (4, 4), (5, 4)]
Minimal example: repl.it/@Rabbid76/PyGame-SnakeMoveInGrid
import pygame
import random
pygame.init()
COLUMNS, ROWS, SIZE = 10, 10, 20
screen = pygame.display.set_mode((COLUMNS*SIZE, ROWS*SIZE))
clock = pygame.time.Clock()
background = pygame.Surface((COLUMNS*SIZE, ROWS*SIZE))
background.fill((255, 255, 255))
for i in range(1, COLUMNS):
pygame.draw.line(background, (128, 128, 128), (i*SIZE-1, 0), (i*SIZE-1, ROWS*SIZE), 2)
for i in range(1, ROWS):
pygame.draw.line(background, (128, 128, 128), (0, i*SIZE-1), (COLUMNS*SIZE, i*SIZE-1), 2)
def random_pos(body):
while True:
pos = random.randrange(COLUMNS), random.randrange(ROWS)
if pos not in body:
break
return pos
length = 1
body = [(COLUMNS//2, ROWS//2)]
dir = (1, 0)
food = random_pos(body)
run = True
while run:
clock.tick(5)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT: dir = (-1, 0)
elif event.key == pygame.K_RIGHT: dir = (1, 0)
elif event.key == pygame.K_UP: dir = (0, -1)
elif event.key == pygame.K_DOWN: dir = (0, 1)
body.insert(0, body[0][:])
body[0] = (body[0][0] + dir[0]) % COLUMNS, (body[0][1] + dir[1]) % ROWS
if body[0] == food:
food = random_pos(body)
length += 1
while len(body) > length:
del body[-1]
screen.blit(background, (0, 0))
pygame.draw.rect(screen, (255, 0, 255), (food[0]*SIZE, food[1]*SIZE, SIZE, SIZE))
for i, pos in enumerate(body):
color = (255, 0, 0) if i==0 else (0, 192, 0) if (i%2)==0 else (255, 128, 0)
pygame.draw.rect(screen, color, (pos[0]*SIZE, pos[1]*SIZE, SIZE, SIZE))
pygame.display.flip()
Now the snake with completely free positioning.
We have to track all the positions which the snake's head has visited in a list. We have to place the elements of the snakes body on the positions in the list like the pearls of a chain.
The key is, to compute the Euclidean distance between the last element of the body in the chain and the following positions on the track. When an new point with a distance that is large enough is found, then an new pearl (element) is add to the chain (body).
dx, dy = body[-1][0]-pos[0], body[-1][1]-pos[1]
if math.sqrt(dx*dx + dy*dy) >= distance:
body.append(pos)
The following function has 3 arguments. track
is the list of the head positions. no_pearls
is then number of elements of the shakes body and distance
is the Euclidean distance between the elements. The function creates and returns a list of the snakes body positions.
def create_body(track, no_pearls, distance):
body = [(track[0])]
track_i = 1
for i in range(1, no_pearls):
while track_i < len(track):
pos = track[track_i]
track_i += 1
dx, dy = body[-1][0]-pos[0], body[-1][1]-pos[1]
if math.sqrt(dx*dx + dy*dy) >= distance:
body.append(pos)
break
while len(body) < no_pearls:
body.append(track[-1])
del track[track_i:]
return body
Minimal example: repl.it/@Rabbid76/PyGame-SnakeMoveFree
import pygame
import random
import math
pygame.init()
COLUMNS, ROWS, SIZE = 10, 10, 20
WIDTH, HEIGHT = COLUMNS*SIZE, ROWS*SIZE
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
background = pygame.Surface((WIDTH, HEIGHT))
background.fill((255, 255, 255))
for i in range(1, COLUMNS):
pygame.draw.line(background, (128, 128, 128), (i*SIZE-1, 0), (i*SIZE-1, ROWS*SIZE), 2)
for i in range(1, ROWS):
pygame.draw.line(background, (128, 128, 128), (0, i*SIZE-1), (COLUMNS*SIZE, i*SIZE-1), 2)
def hit(pos_a, pos_b, distance):
dx, dy = pos_a[0]-pos_b[0], pos_a[1]-pos_b[1]
return math.sqrt(dx*dx + dy*dy) < distance
def random_pos(body):
pos = None
while True:
pos = random.randint(SIZE//2, WIDTH-SIZE//2), random.randint(SIZE//2, HEIGHT-SIZE//2)
if not any([hit(pos, bpos, 20) for bpos in body]):
break
return pos
def create_body(track, no_pearls, distance):
body = [(track[0])]
track_i = 1
for i in range(1, no_pearls):
while track_i < len(track):
pos = track[track_i]
track_i += 1
dx, dy = body[-1][0]-pos[0], body[-1][1]-pos[1]
if math.sqrt(dx*dx + dy*dy) >= distance:
body.append(pos)
break
while len(body) < no_pearls:
body.append(track[-1])
del track[track_i:]
return body
length = 1
track = [(WIDTH//2, HEIGHT//2)]
dir = (1, 0)
food = random_pos(track)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_LEFT: dir = (-1, 0)
elif event.key == pygame.K_RIGHT: dir = (1, 0)
elif event.key == pygame.K_UP: dir = (0, -1)
elif event.key == pygame.K_DOWN: dir = (0, 1)
track.insert(0, track[0][:])
track[0] = (track[0][0] + dir[0]) % WIDTH, (track[0][1] + dir[1]) % HEIGHT
body = create_body(track, length, 20)
if hit(body[0], food, 20):
food = random_pos(body)
length += 1
screen.blit(background, (0, 0))
pygame.draw.circle(screen, (255, 0, 255), food, SIZE//2)
for i, pos in enumerate(body):
color = (255, 0, 0) if i==0 else (0, 192, 0) if (i%2)==0 else (255, 128, 0)
pygame.draw.circle(screen, color, pos, SIZE//2)
pygame.display.flip()