Как мне связать движение тела змеи?

#python-3.x #math #tkinter #random #pygame

#python #pygame

Вопрос:

Я хочу реализовать игру со змеями. Змея извивается по игровой площадке. Каждый раз, когда змея ест пищу, длина змеи увеличивается на один элемент. Элементы тела змеи следуют за его головой, как цепочка.

 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))

    # [...]
 

Как мне добиться, чтобы части тела следовали за головой змеи на ее пути, когда голова змеи движется вперед?

Ответ №1:

В общем, вы должны различать 2 разных типа змеи. В первом случае змея движется в сетке, и каждый раз, когда змея движется, она продвигается вперед на одно поле в сетке. В другом типе позиция змеи не находится в растре и не привязана к полям сетки, позиция свободна, и змея плавно скользит по полям.
В первом случае каждый элемент тела привязан к полям сетки, как и голова. Другой — это еще один трюк, потому что положение элемента body зависит от размера элемента и динамических предыдущих положений головы змеи.


Сначала змея, которая привязана к сетке.

Элементы змеи могут храниться в списке кортежей. Каждый кортеж содержит столбец и строку элемента snakes в сетке. Изменения элементов в списке непосредственно следуют за движением змеи. Если змея движется, новая позиция добавляется в начало списка, а хвост списка удаляется.

Например, у нас есть змея со следующими элементами:

 body = [(3, 3), (3, 4), (4, 4), (5, 4), (6, 4)]
 

Когда голова змеи перемещается из формы (3, 3) в ( 3, 2 ), новая позиция головы добавляется в начало списка ( body.insert(0, (3, 2) ):

 body = [(3, 2), (3, 3), (3, 4), (4, 4), (5, 4), (6, 4)]
 

Наконец, хвост ist удаляется ( del body[-1] ):

 body = [(3, 2), (3, 3), (3, 4), (4, 4), (5, 4)]
 

Минимальный пример: 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()
 

Теперь змея с полностью свободным позиционированием.

Мы должны отслеживать все позиции, которые посетила голова змеи, в списке. Мы должны разместить элементы тела змеи на позициях в списке, как жемчужины цепочки.

Ключ в том, чтобы вычислить евклидово расстояние между последним элементом тела в цепочке и следующими позициями на дорожке. Когда найдена новая точка с достаточно большим расстоянием, в цепочку (тело) добавляется новая жемчужина (элемент).

 dx, dy = body[-1][0]-pos[0], body[-1][1]-pos[1]
if math.sqrt(dx*dx   dy*dy) >= distance:
    body.append(pos)
 

Следующая функция имеет 3 аргумента. track это список позиций головы. no_pearls тогда это количество элементов тела встряхивания и distance евклидово расстояние между элементами. Функция создает и возвращает список положений тела змеи.

 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
 

Минимальный пример: 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()
 

Комментарии:

1. Отличный ответ! Этот 2-й пример придает змее жутковатое движение, которое отражает то, как я воспринимаю их в реальной жизни.