1

I finally figured out how to add body parts to my snake, but they add on in an unusual way. I have been struggling on this for a while, and finally made it so they append. But they don't do it correctly. It seems like they append 1 pixel behind instead of a full bodies length. Does anyone know why?

# Constants
WIN_WIDTH = 500
WIN_HEIGHT = 600
HALF_WIN_WIDTH = WIN_WIDTH / 2
HALF_WIN_HEIGHT = WIN_HEIGHT / 2
FPS = 10

# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
DARK_GREEN = (0, 100, 0)
YELLOW = (255, 255, 0)

# Variables
screen = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
pygame.display.set_caption("Snake")
clock = pygame.time.Clock()
running = True


class Text:

    def __init__(self, x, y, size, font, color, text):
        self.x = x
        self.y = y
        self.size = size
        self.font = font
        self.color = color
        self.text = text

    def draw(self):
        self.my_font = pygame.font.SysFont(self.font, self.size)
        self.text_surface = self.my_font.render(self.text, True, self.color)
        screen.blit(self.text_surface, (self.x, self.y))


class Food:

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.width = 25
        self.height = 25

    def draw(self):
        self.rect = (self.x, self.y, self.width, self.height)
        pygame.draw.rect(screen, BLUE, self.rect)

    def events(self):
        pass

    def update(self):
        pass


class Body:

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.width = 25
        self.height = 25

    def draw(self):
        self.rect = pygame.Rect(self.x, self.y, self.width, self.height)
        pygame.draw.rect(screen, YELLOW, self.rect)


# Snake class
class Snake:

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.width = 25
        self.height = 25
        self.direction = 1
        self.kill = False
        self.collide = False
        self.speed = 3
        self.score = 0
        self.bodies = []

    def draw(self):
        self.rect = pygame.Rect(self.x, self.y, self.width, self.height)
        pygame.draw.rect(screen, BLACK, self.rect)

    def events(self):
        # change direction on key press
        self.keys = pygame.key.get_pressed()

        if self.keys[pygame.K_UP] and self.direction != 3:
            self.direction = 1
        if self.keys[pygame.K_DOWN] and self.direction != 1:
            self.direction = 3
        if self.keys[pygame.K_LEFT] and self.direction != 2:
            self.direction = 4
        if self.keys[pygame.K_RIGHT] and self.direction != 4:
            self.direction = 2

        if self.rect.colliderect(food.rect):
            self.speed += 0.5
            food.x = random.randint(0, WIN_WIDTH)
            food.y = random.randint(0, WIN_HEIGHT)
            self.score += 5
            self.colliide = False
            self.bodies.append(Body(0, 0))

        # Move the end bodies first in reverse order
        for i in range(len(self.bodies)-1, 0, -1):
            x = snake.bodies[i-1].x
            y = snake.bodies[i-1].y
            snake.bodies[i].x = x
            snake.bodies[i].y = y
            snake.bodies[i].draw()

        # Move body 0 to where the head is
        if len(snake.bodies) > 0:
            x = snake.x
            y = snake.y
            snake.bodies[0].x = x
            snake.bodies[0].y = y
            snake.bodies[0].draw()


    def update(self):
        # move
        if self.direction == 1:
            self.y -= self.speed
        if self.direction == 2:
            self.x += self.speed
        if self.direction == 3:
            self.y += self.speed
        if self.direction == 4:
            self.x -= self.speed

        # if on edge of screen
        if self.rect.right > WIN_WIDTH:
            self.kill = True
        if self.x < 0:
            self.kill = True
        if self.y < 0:
            self.kill = True
        if self.rect.bottom > WIN_HEIGHT:
            self.kill = True


# Create the snake object
snake = Snake(HALF_WIN_WIDTH, HALF_WIN_HEIGHT)
food = Food(random.randint(0, WIN_WIDTH), random.randint(0, WIN_HEIGHT))

# Main Loop
while running:
    score_text = Text(220, 5, 40, 'arial', WHITE, f'Score: {snake.score}')
    # Draw
    screen.fill(DARK_GREEN)
    snake.draw()
    food.draw()
    score_text.draw()

    # Event handling
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    if snake.kill:
        running = False

    snake.events()

    # Update
    snake.update()
    food.update()
    clock.tick(60)
    pygame.display.update()

Thank you very much!

  • 1
    Snake is usually a grid based game; your game is not. Is this on purpose? This will make the game way more complex. – sloth Mar 30 '20 at 08:48
  • @sloth, it wasn't really on purpose, but I have no idea why it's placing very close to the head. Is there some way we can place it the length of a segment behind? – I_keep_getting_downvoted Mar 30 '20 at 09:44

1 Answers1

1

You have to track the positions which have been met by the snake. Add a list attribute self.position to the class Snake:

class Snake:

    def __init__(self, x, y):
        # [...]

        self.positions = [(self.x, self.y)]

    # [...]

Add the new position to the list when the snake moves:

class Snake:
    # [...]

    def update(self):

        # move
        if self.direction == 1:
            self.y -= self.speed
        if self.direction == 2:
            self.x += self.speed
        if self.direction == 3:
            self.y += self.speed
        if self.direction == 4:
            self.x -= self.speed

        # add ne position
        if self.x != self.positions[0][0] or self.y != self.positions[0][1]:
            self.positions.insert(0, (self.x, self.y))

Update the x and y coordinate of the body along the stored positions in events. Define a distance between the parts of the body (e.g. 35). And use a method getPos to get the position of a part, by its index:

class Snake:
    # [...]

    def events(self):
        # change direction on key press
        self.keys = pygame.key.get_pressed()

        if self.keys[pygame.K_UP] and self.direction != 3:
            self.direction = 1
        if self.keys[pygame.K_DOWN] and self.direction != 1:
            self.direction = 3
        if self.keys[pygame.K_LEFT] and self.direction != 2:
            self.direction = 4
        if self.keys[pygame.K_RIGHT] and self.direction != 4:
            self.direction = 2

        if self.rect.colliderect(food.rect):
            self.speed += 0.5
            food.x = random.randint(100, WIN_WIDTH - 125)
            food.y = random.randint(150, WIN_HEIGHT - 175)
            self.score += 5
            self.colliide = False
            self.bodies.append(Body(0, 0))

        # Move the end bodies first in reverse order
        for i in range(len(self.bodies)):
            pos = self.getPos(i+1, 35, i == len(self.bodies)-1)
            snake.bodies[i].x = pos[0]
            snake.bodies[i].y = pos[1]
            snake.bodies[i].draw()

The arguments to method getPos are the index of the body part, the distance between the parts and delToEnd. delToEnd becomes true, when the last part of the body is get and indicates, that the positions at the end of the list, which are "behind" the last part of the snake can be deleted:

class Snake:
    # [...]

    def getPos(self, i, dist, delToEnd):
        lenToI = i * dist
        lenAct = 0
        px, py = self.positions[-1]
        for j in range(len(self.positions)-1):
            px, py = self.positions[j]
            pnx, pny = self.positions[j+1]
            delta = math.sqrt((px-pnx)*(px-pnx) + (py-pny)*(py-pny))
            lenAct += delta
            if lenAct >= lenToI:
                w = (lenAct - lenToI) / delta
                px = pnx - (pnx-px) * w
                py = pny - (pny-py) * w
                if delToEnd:
                    del self.positions[j:]
                break
        return (round(px), round(py))

Rabbid76
  • 202,892
  • 27
  • 131
  • 174