cyz1996
cyz1996

Reputation: 11

pygame snake game, snake doesn't move when rapidly changing direction

I just wrote a snake game using pygame module.

After testing, I found that when I rapidly change the snake direction. E.g. pressing two arrow keys very fast to move snake body to next line or change to opposite direction, the snake doesn't respond accurately. Most of the time it will work, but there are few times snake doesn't move. I believe this is because of the low FPS, but if I increase it, the snake will move so fast.

Here is the code:

# snake game

import pygame, sys, random, time

# game initialization
check_errors = pygame.init()

if check_errors[1] > 0:
    print('(!) Got {0} errors during initializing pygame \
        exiting...'.format(check_errors[1]))
    sys.exit(-1)
else:
    print('(+) pygame successfully initialized.')

# game screen
screen_width = 750
screen_height = 495
game_screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption('Snake game')

# colors
red = pygame.Color(255, 0, 0)  # game over
green = pygame.Color(0, 255, 0)  # snake body
black = pygame.Color(0, 0, 0)  # player score
white = pygame.Color(255, 255, 255)  # game background
brown = pygame.Color(165, 42, 42)  # food

# FPS controller
fps_controller = pygame.time.Clock()

# game variables
start_x = 300
start_y = 150
step = 15  # block width is 10
initial_body_length = 3
snake_head = [start_x, start_y]  # snake start position [x, y]
# initialize snake body, index 0 contains the snake head
snake_body = [[start_x - i * step, start_y] for i in range(initial_body_length)]
score = 0
level = 1

food_pos = [random.randrange(2, screen_width / step - 1) * step, \
            random.randrange(2, screen_height / step - 1) * step]  # don't put food at the border of the screen
food_spawn = True

direction = 'RIGHT'
next_direction = direction  # new direction after user hits keyboard


def draw_game_menu():
    count = 3
    my_font = pygame.font.SysFont('monaco', 60)
    while True:
        game_screen.fill(white)

        start_surface = my_font.render('Start in {0} seconds.'.format(count), True, black)
        start_rect = start_surface.get_rect()
        start_rect.midtop = (screen_width / 2, 80)
        game_screen.blit(start_surface, start_rect)

        esc_surface = my_font.render('''Press Esc to exit during game.''', True, black)
        esc_rect = esc_surface.get_rect()
        esc_rect.midtop = (screen_width / 2, 150)
        game_screen.blit(esc_surface, esc_rect)

        pause_surface = my_font.render('''Press Space to pause the game.''', True, black)
        pause_rect = pause_surface.get_rect()
        pause_rect.midtop = (screen_width / 2, 220)
        game_screen.blit(pause_surface, pause_rect)

        pygame.display.flip()  # update the game screen
        time.sleep(1)
        fps_controller.tick()
        count -= 1
        if count == 0: break

def draw_game_pause():
    my_font = pygame.font.SysFont('monaco', 40)
    while True:
        pause_surface = my_font.render('Press Space to continue.', True, black)
        pause_rect = pause_surface.get_rect()
        pause_rect.midtop = (screen_width / 2, 150)
        game_screen.blit(pause_surface, pause_rect)
        pygame.display.flip()
        fps_controller.tick()
        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE: return

def show_score(game_over=False):
    my_font = pygame.font.SysFont('monaco', 40)
    score_surface = my_font.render('Score: {0}'.format(score), True, black)
    score_rect = score_surface.get_rect()
    if game_over == False:
        score_rect.midtop = (75, 10)
    else:
        score_rect.midtop = (screen_width / 2, 130)
    game_screen.blit(score_surface, score_rect)

# game over function
def draw_game_over():
    my_font = pygame.font.SysFont('monaco', 60)
    GO_surface = my_font.render('Game Over !', True, red)
    GO_rect = GO_surface.get_rect()
    GO_rect.midtop = (screen_width/2, 60)
    game_screen.blit(GO_surface, GO_rect)

    show_score(game_over=True)
    pygame.display.flip()  # update the game screen

    time.sleep(4)
    pygame.quit()  # quit the game
    sys.exit()  # exit the console

def get_food(food_pos, snake_body):
    for block in snake_body:
        if block[0] == food_pos[0] and block[1] == food_pos[1]:
            return True
    return False


# game start menu
draw_game_menu()

# main logic of the game
while True:
    for event in pygame.event.get():
        if event.type == pygame.KEYDOWN:  # if user press any button
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                next_direction = 'RIGHT'
            elif event.key == pygame.K_LEFT or event.key == ord('a'):
                next_direction = 'LEFT'
            elif event.key == pygame.K_UP or event.key == ord('w'):
                next_direction = 'UP'
            elif event.key == pygame.K_DOWN or event.key == ord('s'):
                next_direction = 'DOWN'
            elif event.key == pygame.K_ESCAPE:  # if user choose to quit the game
                pygame.event.post(pygame.event.Event(pygame.QUIT))
            elif event.key == pygame.K_SPACE:
                draw_game_pause()
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    # validation of direction
    if next_direction == 'RIGHT' and direction != 'LEFT':
        direction = 'RIGHT'
    elif next_direction == 'LEFT' and direction != 'RIGHT':
        direction = 'LEFT'
    elif next_direction == 'DOWN' and direction != 'UP':
        direction = 'DOWN'
    elif next_direction == 'UP' and direction != 'DOWN':
        direction = 'UP'

    # move snake head
    if direction == 'RIGHT':
        snake_head[0] += step
    elif direction == 'LEFT':
        snake_head[0] -= step
    elif direction == 'DOWN':
        snake_head[1] += step
    elif direction == 'UP':
        snake_head[1] -= step

    # move snake body mechanism
    #   1. insert a new block at the beginning of the body
    #   2. check if snake has reached a food
    #       2.1 if yes, then keep the modified body
    #       2.2 if not, then delete the end of the body
    snake_body.insert(0, list(snake_head))
    if snake_head[0] == food_pos[0] and snake_head[1] == food_pos[1]:
        food_spawn = False
        score += 1
    else:
        snake_body.pop()

    while food_spawn == False:
        food_pos = [random.randrange(2, screen_width / step - 1) * step,
                    random.randrange(2, screen_height / step - 1) * step]
        if get_food(food_pos, snake_body) == True:
            food_spawn = False
        else:
            food_spawn = True

    # fill game background
    game_screen.fill(white)

    # draw snake body
    for pos in snake_body:
        pygame.draw.rect(game_screen, green, pygame.Rect(pos[0], pos[1], step, step))

    # draw food
    pygame.draw.rect(game_screen, brown, pygame.Rect(food_pos[0], food_pos[1], step, step))

    # check if snake hits the border
    if (snake_head[0] > screen_width - step) or (snake_head[0] < 0) or \
            (snake_head[1] > screen_height - step) or (snake_head[1] < 0):
        draw_game_over()

    # check if snake hits itself
    for block in snake_body[1:]:
        if snake_head[0] == block[0] and snake_head[1] == block[1]:
            draw_game_over()

    level = score//5 + 1
    if level > 3:
        level = 3

    show_score(game_over=False)
    pygame.display.flip()

    if level == 1:
        fps_controller.tick(8)
    elif level == 2:
        fps_controller.tick(10)
    elif level == 3:
        fps_controller.tick(12)

Please help to see if there is a way to improve, thanks.

Upvotes: 1

Views: 1302

Answers (1)

sloth
sloth

Reputation: 101052

First of all, you should try to use a single main loop.

While you're rendering the start or end screen, you can't interact with the window because no event loop runs. It's very annoying that you can't move or close the window during that time. Maybe take a look here for an example of how to handle this.

Second, really use a higher framerate, and don't tie the speed of your game objects to the frame rate.

There are several ways to handle this, an example is to use an event that signals when the snake should move. Here's an example I wrote for another question.

Here's a simple implementation for your current code:

MOVE_SNAKE = pygame.USEREVENT
pygame.time.set_timer(MOVE_SNAKE, 300)

# main logic of the game
while True:
    for event in pygame.event.get():
        if event.type == pygame.KEYDOWN:  # if user press any button
            if event.key == pygame.K_RIGHT or event.key == ord('d'):
                next_direction = 'RIGHT'
            elif event.key == pygame.K_LEFT or event.key == ord('a'):
                next_direction = 'LEFT'
            elif event.key == pygame.K_UP or event.key == ord('w'):
                next_direction = 'UP'
            elif event.key == pygame.K_DOWN or event.key == ord('s'):
                next_direction = 'DOWN'
            elif event.key == pygame.K_ESCAPE:  # if user choose to quit the game
                pygame.event.post(pygame.event.Event(pygame.QUIT))
            elif event.key == pygame.K_SPACE:
                draw_game_pause()
        elif event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()
        elif event.type == MOVE_SNAKE:
            # move snake head
            if direction == 'RIGHT':
                snake_head[0] += step
            elif direction == 'LEFT':
                snake_head[0] -= step
            elif direction == 'DOWN':
                snake_head[1] += step
            elif direction == 'UP':
                snake_head[1] -= step

            # move snake body mechanism
            #   1. insert a new block at the beginning of the body
            #   2. check if snake has reached a food
            #       2.1 if yes, then keep the modified body
            #       2.2 if not, then delete the end of the body
            snake_body.insert(0, list(snake_head))
            if snake_head[0] == food_pos[0] and snake_head[1] == food_pos[1]:
                food_spawn = False
                score += 1
            else:
                snake_body.pop()

    # validation of direction
    if next_direction == 'RIGHT' and direction != 'LEFT':
        direction = 'RIGHT'
    elif next_direction == 'LEFT' and direction != 'RIGHT':
        direction = 'LEFT'
    elif next_direction == 'DOWN' and direction != 'UP':
        direction = 'DOWN'
    elif next_direction == 'UP' and direction != 'DOWN':
        direction = 'UP'



    while food_spawn == False:
        food_pos = [random.randrange(2, screen_width / step - 1) * step,
                    random.randrange(2, screen_height / step - 1) * step]
        if get_food(food_pos, snake_body) == True:
            food_spawn = False
        else:
            food_spawn = True

    # fill game background
    game_screen.fill(white)

    # draw snake body
    for pos in snake_body:
        pygame.draw.rect(game_screen, green, pygame.Rect(pos[0], pos[1], step, step))

    # draw food
    pygame.draw.rect(game_screen, brown, pygame.Rect(food_pos[0], food_pos[1], step, step))

    # check if snake hits the border
    if (snake_head[0] > screen_width - step) or (snake_head[0] < 0) or \
            (snake_head[1] > screen_height - step) or (snake_head[1] < 0):
        draw_game_over()

    # check if snake hits itself
    for block in snake_body[1:]:
        if snake_head[0] == block[0] and snake_head[1] == block[1]:
            draw_game_over()

    new_level = score//5 + 1
    if new_level != level:
        pygame.time.set_timer(MOVE_SNAKE, 300 / new_level)
        if new_level <= 3:
            level = new_level

    show_score(game_over=False)
    pygame.display.flip()

    fps_controller.tick(60)

See how easy it is now to control the speed of the snake: it moves now every 300ms.

Upvotes: 2

Related Questions