¿Cómo consigo que la serpiente crezca y encadene el movimiento del cuerpo de la serpiente?

Resuelto Rabbid76 asked hace 4 años • 1 respuestas

Quiero implementar un juego de serpientes. La serpiente deambula por el patio de recreo. Cada vez que la serpiente come algo de comida, su longitud aumenta en un elemento. Los elementos del cuerpo de la serpiente siguen su cabeza como una cadena.

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

    # [...]

¿Cómo logro que las partes del cuerpo sigan la cabeza de la serpiente en su camino, cuando la cabeza de la serpiente avanza?

Rabbid76 avatar May 26 '20 03:05 Rabbid76
Aceptado

En general hay que distinguir entre 2 tipos diferentes de serpiente . En el primer caso, la serpiente se mueve en una cuadrícula y cada vez que se mueve, avanza un campo en la cuadrícula. En el otro tipo, la posición de la serpiente no está en una trama y no está encajada en los campos de la cuadrícula, la posición es libre y la serpiente se desliza suavemente a través de los campos.
En el primero, cada elemento del cuerpo se ajusta a los campos de la cuadrícula, al igual que la cabeza. El otro es más complicado, porque la posición de un elemento del cuerpo depende del tamaño del elemento y de las posiciones dinámicas previas de la cabeza de la serpiente.


Primero la serpiente , que está encajada en una rejilla.

Los elementos de la serpiente se pueden almacenar en una lista de tuplas. Cada tupla contiene la columna y la fila del elemento serpientes en la cuadrícula. Los cambios en los elementos de la lista siguen directamente el movimiento de la serpiente. Si la serpiente se mueve, la nueva posición se agrega al principio de la lista y se elimina la cola de la lista.

Por ejemplo tenemos una serpiente con los siguientes elementos:

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

Cuando la cabeza de la serpiente se mueve (3, 3)de ( 3, 2), entonces la nueva posición de la cabeza se agrega al encabezado de la lista ( body.insert(0, (3, 2)):

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

Finalmente se retira la cola del ist ( del body[-1]):

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

Ejemplo mínimo: 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()

Ahora la serpiente con posicionamiento completamente libre.

Tenemos que rastrear todas las posiciones que ha visitado la cabeza de la serpiente en una lista. Tenemos que colocar los elementos del cuerpo de la serpiente en las posiciones de la lista como las perlas de una cadena.

La clave es calcular la distancia euclidiana entre el último elemento del cuerpo en la cadena y las siguientes posiciones en la pista. Cuando se encuentra un nuevo punto con una distancia lo suficientemente grande, se agrega una nueva perla (elemento) a la cadena (cuerpo).

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

La siguiente función tiene 3 argumentos. trackes la lista de las posiciones principales. no_pearlses entonces el número de elementos del cuerpo del batido y distancees la distancia euclidiana entre los elementos. La función crea y devuelve una lista de las posiciones del cuerpo de las serpientes.

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

Ejemplo mínimo: 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()
Rabbid76 avatar May 25 '2020 20:05 Rabbid76