1

I am having a hard time figuring the physics of speed in this snake game I made using pygame. The issue is that as soon as I set the fps to be above 15, the snake's speed increases as well. I know that this has to do with milliseconds etc which I found to work, high fps with slow speed. However at that point, I could not get the X and Y to be correct so that I can eat the apple. I am pretty much lost at this point. Here's my entire snake game. I don't mind sharing it since I thought to open source it as soon as it's finished.

import pygame
import random
from pygame.locals import (
    K_UP,
    K_DOWN,
    K_LEFT,
    K_RIGHT,
    K_ESCAPE,
    KEYDOWN,
    K_n,
    K_o,
    K_w,
    K_a,
    K_s,
    K_d,
    K_RETURN,
    QUIT,
)

pygame.init()
pygame.display.set_caption("Le jeu snake !")


class Game:
    def __init__(self):
        self.white = (255, 255, 255)
        self.black = (0, 0, 0)
        self.red = (255, 0, 0)
        self.blue = (0, 0, 255)
        self.green = (0, 255, 0)

        self.background = pygame.image.load("snake_bg.png")

        self.SCREEN_WIDTH = 600
        self.SCREEN_HEIGHT = 400

        self.screen = pygame.display.set_mode([self.SCREEN_WIDTH, self. SCREEN_HEIGHT])

        self.running = True
        self.paused = False
        self.agreed = False

        self.snake_block = 25
        self.apple_block = 25

        self.snake_x = 0
        self.snake_y = 0
        self.apple_x = 0
        self.apple_y = 0

        self.center = (self.SCREEN_WIDTH // 2, self.SCREEN_HEIGHT // 2)

        self.width_grid = 0
        self.height_grid = 0

        self.snake_x_change = 0
        self.snake_y_change = 0

        self.snake_list = list()
        self.snake_head = list()
        self.snake_length = 1

        self.score = 0

        self.clock = pygame.time.Clock()

        self.velocity = 25

    def draw_grid(self):
        self.width_grid = [x * 25 for x in range(0, self.SCREEN_WIDTH)]
        self.height_grid = [y * 25 for y in range(0, self.SCREEN_WIDTH)]

        """for grid_x in self.width_grid:
            pygame.draw.line(self.screen, self.white, [0, grid_x], [self.SCREEN_WIDTH, grid_x], 2)
            if grid_x >= 600:
                break
        for grid_y in self.height_grid:
            pygame.draw.line(self.screen, self.white, [grid_y, 0], [grid_y, self.SCREEN_WIDTH], 2)
            if grid_y >= 600:
                break"""

    def set_position(self, thing):
        if thing == "snake":
            self.snake_x = self.SCREEN_WIDTH / 2
            self.snake_y = self.SCREEN_HEIGHT / 2
        if thing == "apple":
            self.apple_x = random.choice(self.width_grid[0:24])
            self.apple_y = random.choice(self.height_grid[0:16])

    def draw(self, obj):
        if obj == "snake":
            for XnY in self.snake_list:
                pygame.draw.rect(self.screen, self.green, (XnY[0], XnY[1], self.snake_block, self.snake_block), 2)
        elif obj == "apple":
            pygame.draw.rect(self.screen, self.red, (self.apple_x, self.apple_y, self.apple_block, self.apple_block))

    def set_keys_direction(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            if event.type == KEYDOWN:
                if event.key == K_DOWN:
                    self.snake_y_change = self.velocity
                    self.snake_x_change = 0
                if event.key == K_RIGHT:
                    self.snake_x_change = self.velocity
                    self.snake_y_change = 0
                if event.key == K_UP:
                    self.snake_y_change = -self.velocity
                    self.snake_x_change = 0
                if event.key == K_LEFT:
                    self.snake_x_change = -self.velocity
                    self.snake_y_change = 0
                if event.key == K_s:
                    self.snake_y_change = self.velocity
                    self.snake_x_change = 0
                if event.key == K_w:
                    self.snake_y_change = -self.velocity
                    self.snake_x_change = 0
                if event.key == K_d:
                    self.snake_x_change = self.velocity
                    self.snake_y_change = 0
                if event.key == K_a:
                    print("Q works")
                    self.snake_x_change = -self.velocity
                    self.snake_y_change = 0
                if not self.agreed:
                    if event.key == K_ESCAPE:
                        pygame.quit()
                    if event.key == K_RETURN:
                        self.agreed = True
                if self.agreed:
                    if event.key == K_n:
                        pygame.quit()
                    if event.key == K_o:
                        self.paused = False
                        self.score = 0
                if event.key == K_ESCAPE:
                    self.snake_length += 1

        self.elapsed = self.clock.tick(30)

    def build_snake(self):
        snake_head = list()
        snake_head.append(self.snake_x)
        snake_head.append(self.snake_y)
        self.snake_list.append(snake_head)

        if len(self.snake_list) > self.snake_length:
            del self.snake_list[0]

        for snake in self.snake_list[:-1]:
            if snake == snake_head:
                self.snake_reset()

        self.draw("snake")

    def check_apple_eaten(self):
        if self.snake_x == self.apple_x and self.snake_y == self.apple_y:
            self.set_position("apple")
            self.snake_length += 1
            self.score += 1

    def snake_borders_check(self):
        if self.snake_x < 0 or self.snake_x > self.SCREEN_WIDTH - 25:
            self.snake_reset()
        if self.snake_y < 0 or self.snake_y > self.SCREEN_HEIGHT - 25:
            self.snake_reset()

    def snake_reset(self):
        self.paused = True
        self.set_position("snake")
        self.set_position("apple")
        del self.snake_list[1:]
        self.snake_length = 1

    def snake_bit_check(self):
        pass

        """if len(self.snake_list) >= 6:
            for snake in self.snake_list[2:]:
                if self.snake_list[0][0] == snake[0] and self.snake_list[0][1] == snake[1]:
                    print("SnakeList[0][0]: {0} || SnakeList[0][1]: {0}".format(self.snake_list[0][0],
                                                                                self.snake_list[0][1]))
                    print("snake: {0}".format(snake))

                    self.snake_reset()"""

    def show_text(self, message, position, font_name="fonts/arial_narrow_7.ttf", font_size=32):
        font = pygame.font.Font(font_name, font_size)
        text = font.render(message, True, self.white, self.black)
        text_rect = text.get_rect(center=position)

        self.screen.blit(text, text_rect)

    def game_over(self):
        self.screen.blit(self.background, (0, 0))
        self.show_text("Ton score: {0}".format(self.score), (100, 20))
        self.show_text("Tu as perdu ! Veux-tu recommencer ?",
                      (self.SCREEN_WIDTH // 2, self.SCREEN_HEIGHT // 2 - 100))
        self.show_text("Appuie sur O (oui) ou sur N (non)",
                      (self.SCREEN_WIDTH // 2, self.SCREEN_HEIGHT // 2))
        self.show_text("Sinon, Echap pour quitter",
                      (self.SCREEN_WIDTH // 2, self.SCREEN_HEIGHT // 2 + 100))

    def check_agreement(self):
        self.show_text("Ceci est une réplique du jeu snake faite",
                      (self.SCREEN_WIDTH // 2, self.SCREEN_HEIGHT // 2 - 100))
        self.show_text("par Kevin pour un simple projet personnel.",
                      (self.SCREEN_WIDTH // 2 + 1, self.SCREEN_HEIGHT // 2 - 70))
        self.show_text("Si vous voulez jouer au jeu, appuyez sur la",
                      (self.SCREEN_WIDTH // 2 + 5, self.SCREEN_HEIGHT // 2 - 20))
        self.show_text("touche Entrée, sinon Echap pour quitter !",
                      (self.SCREEN_WIDTH // 2 - 11, self.SCREEN_HEIGHT // 2 + 10))
        self.show_text("- Utilise les touches flèches pour changer de direction",
                      (self.SCREEN_WIDTH // 2 - 10, self.SCREEN_HEIGHT // 2 + 55), font_size=24)
        self.show_text("- Ne te mord pas toi-même",
                      (self.SCREEN_WIDTH // 2 - 144, self.SCREEN_HEIGHT // 2 + 75), font_size=24)
        self.show_text("- Ne rentre pas dans les bords",
                      (self.SCREEN_WIDTH // 2 - 128, self.SCREEN_HEIGHT // 2 + 95), font_size=24)

    def game(self):
        self.draw_grid()
        self.set_position("snake")
        self.set_position("apple")

        while self.running:
            self.screen.blit(self.background, (0, 0))
            self.set_keys_direction()

            if self.agreed:
                self.draw_grid()

                self.draw("apple")

                self.build_snake()

                self.check_apple_eaten()

                self.snake_bit_check()

                self.snake_borders_check()
            else:
                self.check_agreement()

            if not self.paused:
                self.snake_x += self.snake_x_change
                self.snake_y += self.snake_y_change
            else:
                self.game_over()

            self.clock.tick(30)

            pygame.display.flip()


game = Game()

game.game()

pygame.quit()
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
Kevin
  • 364
  • 1
  • 2
  • 13
  • Im not into pygame but im sure you should multiply the velocity by the delta time – vikAy Apr 04 '20 at 20:44
  • As I said, doing so will actually change the x and y positions of my snake in a way that makes it almost impossible to aim for the apple’s coordinates. – Kevin Apr 04 '20 at 21:07
  • i think you should consider separating the snake movement with the games fps. You can have constant velocity on the snake based on real time, and just updating your game or player actions on clock ticks. This is also related https://stackoverflow.com/questions/34383559/pygame-clock-tick-vs-framerate-in-game-main-loop – 001001 Apr 04 '20 at 21:14

1 Answers1

1

The return value of self.clock.tick() is the time which has passed since the last call. Use the return value to control the speed. Define the distance, of movement of the snake per second (e.g. self.velocity = 400 means 400 pixel per second). Get the time between to frames (delta_t) and scale the movement of the snake by the elapsed time (delta_t / 1000):

class Game:
    def __init__(self):
        # [...]

        # distance per second 
        self.velocity = 400

    # [...]

    def game(self):

        # [...]

        while self.running:
            delta_t = self.clock.tick(30)

            # [...]

            if not self.paused:
                step = delta_t / 1000 # / 1000 because unit of velocity is seconds
                self.snake_x += self.snake_x_change * step
                self.snake_y += self.snake_y_change * step
            else:
                self.game_over()

            pygame.display.flip()

With this setup it is ease to control the speed of the snake. For instance, the speed can be increased (e.g. self.velocity += 50), when the snake grows.

Of course you have to round the position of the snake (self.snake_x, self.snake_y) to a multiple of the grid size (multiple of 25) when you draw the snake and when you do the collision test. Use round to do so:

x, y = round(self.snake_x / 25) * 25, round(self.snake_y / 25) * 25

Ensure that the positions which are stored in snake_list are a multiple of 25. Just append a new head to the list, if the head of the snake has reached a new field:

if len(self.snake_list) <= 0 or snake_head != self.snake_list[-1]:
   self.snake_list.append(snake_head)

Apply that to the methods build_snake draw and check_apple_eaten:

class Game:
    # [...]

    def build_snake(self):
        snake_head = list()
        x, y = round(self.snake_x / 25) * 25, round(self.snake_y / 25) * 25
        snake_head.append(x)
        snake_head.append(y)
        if len(self.snake_list) <= 0 or snake_head != self.snake_list[-1]:
            self.snake_list.append(snake_head)

        if len(self.snake_list) > self.snake_length:
            del self.snake_list[0]

        for snake in self.snake_list[:-1]:
            if snake == snake_head:
                self.snake_reset()

        self.draw("snake")

    def check_apple_eaten(self):
        x, y = round(self.snake_x / 25) * 25, round(self.snake_y / 25) * 25
        if x == self.apple_x and y == self.apple_y:
            self.set_position("apple")
            self.snake_length += 1
            self.score += 1

    def snake_borders_check(self):
        x, y = round(self.snake_x / 25) * 25, round(self.snake_y / 25) * 25
        if x < 0 or x > self.SCREEN_WIDTH - 25:
            self.snake_reset()
        if y < 0 or y > self.SCREEN_HEIGHT - 25:
            self.snake_reset()
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
  • Well this actually worked. Now I am having a hard time figuring the snake biting himself, I am doing this in the build_snake method in the for loop. I tried rounding x,y etc and appending x and y instead of self.snake_y but it's biting himself as soon as he grows.. – Kevin Apr 05 '20 at 09:14
  • No no, I should've removed that method since it is pointless for now. The code that worked was the for loop in "def build_snake(self)". The "for snake in self.snake_list[:-1]" block. – Kevin Apr 05 '20 at 09:30
  • 1
    @Twizzler I had to do some investigations. Finally I've fixed the issue. I've changed the answer. See the last code snippet. – Rabbid76 Apr 05 '20 at 09:58
  • 1
    well that worked perfectly well. thank you very much ! the game is fully functional now – Kevin Apr 05 '20 at 10:14