Пишем змейку на pygame
Есть такая библиотека для разработки двухмерных игр - pygame. Недавно я прошел курс на образовательном сайте udemy - Python Game Development : Creating a Snake Game from scratch. Этот курс посвещен созданию классической змейки на pygame. Хотел бы поделиться получившейся игрой. Весь код в данном посте я взял из данного курса, правда изрядно его изменил, в частности добавил использование классов. Также я внес несколько исправлений.
Установить Pygame можно через pip:
pip install pygame
Начнем с импортов:
1 import pygame 2 import sys 3 import random 4 import time
Будем использовать собственно pygame, sys ради функции exit - чтобы завершать скрипт при ошибке или game over'e, random - ради функции randrange, чтобы размещать еду в случайных местах и time - ради функции sleep, чтобы экран игры немного задержался при game over'e.
Я разбил игру на 3 класса: Game - в котором находятся настройки игры, методы для инициализации, завершения игры, отрбражения результата, Snake - здесь параметры отвечающие за позицию головы змеи, координаты тела змеи, метод, который обеспечивает изменения тела (добавления и убирания сегментов) и Food - параметр положения еды, метод отображения еды.
Класс Game:
1 class Game(): 2 def __init__(self): 3 # задаем размеры экрана 4 self.screen_width = 720 5 self.screen_height = 460 6 7 # необходимые цвета 8 self.red = pygame.Color(255, 0, 0) 9 self.green = pygame.Color(0, 255, 0) 10 self.black = pygame.Color(0, 0, 0) 11 self.white = pygame.Color(255, 255, 255) 12 self.brown = pygame.Color(165, 42, 42) 13 14 # Frame per second controller 15 # будет задавать количество кадров в секунду 16 self.fps_controller = pygame.time.Clock() 17 18 # переменная для оторбражения результата 19 # (сколько еды съели) 20 self.score = 0 21 22 def init_and_check_for_errors(self): 23 """Начальная функция для инициализации и 24 проверки как запустится pygame""" 25 check_errors = pygame.init() 26 if check_errors[1] > 0: 27 sys.exit() 28 else: 29 print('Ok') 30 31 def set_surface_and_title(self): 32 """Задаем surface(поверхность поверх которой будет все рисоваться) 33 и устанавливаем загаловок окна""" 34 self.play_surface = pygame.display.set_mode(( 35 self.screen_width, self.screen_height)) 36 pygame.display.set_caption('Snake Game') 37 38 def event_loop(self, change_to): 39 """Функция для отслеживания нажатий клавиш игроком""" 40 41 # запускаем цикл по ивентам 42 for event in pygame.event.get(): 43 # если нажали клавишу 44 if event.type == pygame.KEYDOWN: 45 if event.key == pygame.K_RIGHT or event.key == ord('d'): 46 change_to = "RIGHT" 47 elif event.key == pygame.K_LEFT or event.key == ord('a'): 48 change_to = "LEFT" 49 elif event.key == pygame.K_UP or event.key == ord('w'): 50 change_to = "UP" 51 elif event.key == pygame.K_DOWN or event.key == ord('s'): 52 change_to = "DOWN" 53 # нажали escape 54 elif event.key == pygame.K_ESCAPE: 55 pygame.quit() 56 sys.exit() 57 return change_to 58 59 def refresh_screen(self): 60 """обновляем экран и задаем фпс""" 61 pygame.display.flip() 62 game.fps_controller.tick(23) 63 64 def show_score(self, choice=1): 65 """Отображение результата""" 66 s_font = pygame.font.SysFont('monaco', 24) 67 s_surf = s_font.render( 68 'Score: {0}'.format(self.score), True, self.black) 69 s_rect = s_surf.get_rect() 70 # дефолтный случай отображаем результат слева сверху 71 if choice == 1: 72 s_rect.midtop = (80, 10) 73 # при game_overe отображаем результат по центру 74 # под надписью game over 75 else: 76 s_rect.midtop = (360, 120) 77 # рисуем прямоугольник поверх surface 78 self.play_surface.blit(s_surf, s_rect) 79 80 def game_over(self): 81 """Функция для вывода надписи Game Over и результатов 82 в случае завершения игры и выход из игры""" 83 go_font = pygame.font.SysFont('monaco', 72) 84 go_surf = go_font.render('Game over', True, self.red) 85 go_rect = go_surf.get_rect() 86 go_rect.midtop = (360, 15) 87 self.play_surface.blit(go_surf, go_rect) 88 self.show_score(0) 89 pygame.display.flip() 90 time.sleep(3) 91 pygame.quit() 92 sys.exit()
Я добавил побольше комментариев, чтобы объяснить код. На строке 25 в переменной check_errors будет кортеж вида (6, 0) - (количество завершенных тасков, количество ошибок), поэтому на строке 26 берется второй элемент кортежа и проверяется на количество ошибок. В функции event_loop и в фукции game_over я сначала убиваю окно pygame функцией quit(), а затем уже выхожу из скрипта sys.exit'ом. В функции show_score переменная choice нужна чтобы определить как выводить результат - слева сверху в течение игры (стр. 72) или по центру в момент завершения (стр. 76).
Далее идет класс Snake:
1 class Snake(): 2 def __init__(self, snake_color): 3 # важные переменные - позиция головы змеи и его тела 4 self.snake_head_pos = [100, 50] # [x, y] 5 # начальное тело змеи состоит из трех сегментов 6 # голова змеи - первый элемент, хвост - последний 7 self.snake_body = [[100, 50], [90, 50], [80, 50]] 8 self.snake_color = snake_color 9 # направление движение змеи, изначально 10 # зададимся вправо 11 self.direction = "RIGHT" 12 # куда будет меняться напрвление движения змеи 13 # при нажатии соответствующих клавиш 14 self.change_to = self.direction 15 16 def validate_direction_and_change(self): 17 """Изменияем направление движения змеи только в том случае, 18 если оно не прямо противоположно текущему""" 19 if any((self.change_to == "RIGHT" and not self.direction == "LEFT", 20 self.change_to == "LEFT" and not self.direction == "RIGHT", 21 self.change_to == "UP" and not self.direction == "DOWN", 22 self.change_to == "DOWN" and not self.direction == "UP")): 23 self.direction = self.change_to 24 25 def change_head_position(self): 26 """Изменияем положение головы змеи""" 27 if self.direction == "RIGHT": 28 self.snake_head_pos[0] += 10 29 elif self.direction == "LEFT": 30 self.snake_head_pos[0] -= 10 31 elif self.direction == "UP": 32 self.snake_head_pos[1] -= 10 33 elif self.direction == "DOWN": 34 self.snake_head_pos[1] += 10 35 36 def snake_body_mechanism( 37 self, score, food_pos, screen_width, screen_height): 38 # если вставлять просто snake_head_pos, 39 # то на всех трех позициях в snake_body 40 # окажется один и тот же список с одинаковыми координатами 41 # и мы будем управлять змеей из одного квадрата 42 self.snake_body.insert(0, list(self.snake_head_pos)) 43 # если съели еду 44 if (self.snake_head_pos[0] == food_pos[0] and 45 self.snake_head_pos[1] == food_pos[1]): 46 # если съели еду то задаем новое положение еды случайным 47 # образом и увеличивем score на один 48 food_pos = [random.randrange(1, screen_width/10)*10, 49 random.randrange(1, screen_height/10)*10] 50 score += 1 51 else: 52 # если не нашли еду, то убираем последний сегмент, 53 # если этого не сделать, то змея будет постоянно расти 54 self.snake_body.pop() 55 return score, food_pos 56 57 def draw_snake(self, play_surface, surface_color): 58 """Отображаем все сегменты змеи""" 59 play_surface.fill(surface_color) 60 for pos in self.snake_body: 61 # pygame.Rect(x,y, sizex, sizey) 62 pygame.draw.rect( 63 play_surface, self.snake_color, pygame.Rect( 64 pos[0], pos[1], 10, 10)) 65 66 def check_for_boundaries(self, game_over, screen_width, screen_height): 67 """Проверка, что столкунлись с концами экрана или сами с собой 68 (змея закольцевалась)""" 69 if any(( 70 self.snake_head_pos[0] > screen_width-10 71 or self.snake_head_pos[0] < 0, 72 self.snake_head_pos[1] > screen_height-10 73 or self.snake_head_pos[1] < 0 74 )): 75 game_over() 76 for block in self.snake_body[1:]: 77 # проверка на то, что первый элемент(голова) врезался в 78 # любой другой элемент змеи (закольцевались) 79 if (block[0] == self.snake_head_pos[0] and 80 block[1] == self.snake_head_pos[1]): 81 game_over()
В конструкторе класса стр.2-стр.7 задаются две переменные, отвечающие за положение змеи (головы и сегментов). В функции validate_direction_and_change направление движения змеи изменяется только тогда, когда игрок не нажал противоположное направление - так нельзя делать в классической змейке, поворачивать можно только под прямым углом. В функции snake_body_mechanism, в которой описан механизм изменения тела змеи, есть важный момент - в строке 42 обязательно создавать новый список (делать list(self.snake_head_pos)), иначе (если сделать просто self.snake_body.insert(0, self.snake_head_pos) ) будет передан не новый список, а тот же самый список snake_head_pos и через несколько итераций мы будем управлять одной точкой, после чего игра завершится game_over'ом, предлагаю вам проверить это самим и подумать почему так получается.
Позиция еды на строке 48 задается таким образом, чтобы x и y еды были всегда кратны 10, чтобы всегда была возможность ее захватить. Начало координат находится в левом верхнем углу, причем ось x напрвлено вправо, а ось y - вниз, поэтому на строках 28, 30, 32 и 34 координаты изменяются соответствующим образом.
Класс Food:
1 class Food(): 2 def __init__(self, food_color, screen_width, screen_height): 3 """Инит еды""" 4 self.food_color = food_color 5 self.food_size_x = 10 6 self.food_size_y = 10 7 self.food_pos = [random.randrange(1, screen_width/10)*10, 8 random.randrange(1, screen_height/10)*10] 9 10 def draw_food(self, play_surface): 11 """Отображение еды""" 12 pygame.draw.rect( 13 play_surface, self.food_color, pygame.Rect( 14 self.food_pos[0], self.food_pos[1], 15 self.food_size_x, self.food_size_y))
Тут все понятно, единственное, в методе draw_food используется метод rect(), который принимает 3 аргумента: поверхность, цвет и объект, в данном случае Rect(x,y, sizex, sizey).
Далее создаем классы и инициализиурем Pygame:
1 game = Game() 2 snake = Snake(game.green) 3 food = Food(game.brown, game.screen_width, game.screen_height) 4 5 game.init_and_check_for_errors() 6 game.set_surface_and_title()
Далее запускаем бесконечный цикл, в котором последовательно вызыаем описанные методы классов с соответствующими параметрами:
1 while True: 2 snake.change_to = game.event_loop(snake.change_to) 3 4 snake.validate_direction_and_change() 5 snake.change_head_position() 6 game.score, food.food_pos = snake.snake_body_mechanism( 7 game.score, food.food_pos, game.screen_width, game.screen_height) 8 snake.draw_snake(game.play_surface, game.white) 9 10 food.draw_food(game.play_surface) 11 12 snake.check_for_boundaries( 13 game.game_over, game.screen_width, game.screen_height) 14 15 game.show_score() 16 game.refresh_screen()
Итого получилось:
1 import pygame 2 import sys 3 import random 4 import time 5 6 7 class Game(): 8 def __init__(self): 9 # задаем размеры экрана 10 self.screen_width = 720 11 self.screen_height = 460 12 13 # необходимые цвета 14 self.red = pygame.Color(255, 0, 0) 15 self.green = pygame.Color(0, 255, 0) 16 self.black = pygame.Color(0, 0, 0) 17 self.white = pygame.Color(255, 255, 255) 18 self.brown = pygame.Color(165, 42, 42) 19 20 # Frame per second controller 21 # будет задавать количество кадров в секунду 22 self.fps_controller = pygame.time.Clock() 23 24 # переменная для оторбражения результата 25 # (сколько еды съели) 26 self.score = 0 27 28 def init_and_check_for_errors(self): 29 """Начальная функция для инициализации и 30 проверки как запустится pygame""" 31 check_errors = pygame.init() 32 if check_errors[1] > 0: 33 sys.exit() 34 else: 35 print('Ok') 36 37 def set_surface_and_title(self): 38 """Задаем surface(поверхность поверх которой будет все рисоваться) 39 и устанавливаем загаловок окна""" 40 self.play_surface = pygame.display.set_mode(( 41 self.screen_width, self.screen_height)) 42 pygame.display.set_caption('Snake Game') 43 44 def event_loop(self, change_to): 45 """Функция для отслеживания нажатий клавиш игроком""" 46 47 # запускаем цикл по ивентам 48 for event in pygame.event.get(): 49 # если нажали клавишу 50 if event.type == pygame.KEYDOWN: 51 if event.key == pygame.K_RIGHT or event.key == ord('d'): 52 change_to = "RIGHT" 53 elif event.key == pygame.K_LEFT or event.key == ord('a'): 54 change_to = "LEFT" 55 elif event.key == pygame.K_UP or event.key == ord('w'): 56 change_to = "UP" 57 elif event.key == pygame.K_DOWN or event.key == ord('s'): 58 change_to = "DOWN" 59 # нажали escape 60 elif event.key == pygame.K_ESCAPE: 61 pygame.quit() 62 sys.exit() 63 return change_to 64 65 def refresh_screen(self): 66 """обновляем экран и задаем фпс""" 67 pygame.display.flip() 68 game.fps_controller.tick(23) 69 70 def show_score(self, choice=1): 71 """Отображение результата""" 72 s_font = pygame.font.SysFont('monaco', 24) 73 s_surf = s_font.render( 74 'Score: {0}'.format(self.score), True, self.black) 75 s_rect = s_surf.get_rect() 76 # дефолтный случай отображаем результат слева сверху 77 if choice == 1: 78 s_rect.midtop = (80, 10) 79 # при game_overe отображаем результат по центру 80 # под надписью game over 81 else: 82 s_rect.midtop = (360, 120) 83 # рисуем прямоугольник поверх surface 84 self.play_surface.blit(s_surf, s_rect) 85 86 def game_over(self): 87 """Функция для вывода надписи Game Over и результатов 88 в случае завершения игры и выход из игры""" 89 go_font = pygame.font.SysFont('monaco', 72) 90 go_surf = go_font.render('Game over', True, self.red) 91 go_rect = go_surf.get_rect() 92 go_rect.midtop = (360, 15) 93 self.play_surface.blit(go_surf, go_rect) 94 self.show_score(0) 95 pygame.display.flip() 96 time.sleep(3) 97 pygame.quit() 98 sys.exit() 99 100 101 class Snake(): 102 def __init__(self, snake_color): 103 # важные переменные - позиция головы змеи и его тела 104 self.snake_head_pos = [100, 50] # [x, y] 105 # начальное тело змеи состоит из трех сегментов 106 # голова змеи - первый элемент, хвост - последний 107 self.snake_body = [[100, 50], [90, 50], [80, 50]] 108 self.snake_color = snake_color 109 # направление движение змеи, изначально 110 # зададимся вправо 111 self.direction = "RIGHT" 112 # куда будет меняться напрвление движения змеи 113 # при нажатии соответствующих клавиш 114 self.change_to = self.direction 115 116 def validate_direction_and_change(self): 117 """Изменияем направление движения змеи только в том случае, 118 если оно не прямо противоположно текущему""" 119 if any((self.change_to == "RIGHT" and not self.direction == "LEFT", 120 self.change_to == "LEFT" and not self.direction == "RIGHT", 121 self.change_to == "UP" and not self.direction == "DOWN", 122 self.change_to == "DOWN" and not self.direction == "UP")): 123 self.direction = self.change_to 124 125 def change_head_position(self): 126 """Изменияем положение головы змеи""" 127 if self.direction == "RIGHT": 128 self.snake_head_pos[0] += 10 129 elif self.direction == "LEFT": 130 self.snake_head_pos[0] -= 10 131 elif self.direction == "UP": 132 self.snake_head_pos[1] -= 10 133 elif self.direction == "DOWN": 134 self.snake_head_pos[1] += 10 135 136 def snake_body_mechanism( 137 self, score, food_pos, screen_width, screen_height): 138 # если вставлять просто snake_head_pos, 139 # то на всех трех позициях в snake_body 140 # окажется один и тот же список с одинаковыми координатами 141 # и мы будем управлять змеей из одного квадрата 142 self.snake_body.insert(0, list(self.snake_head_pos)) 143 # если съели еду 144 if (self.snake_head_pos[0] == food_pos[0] and 145 self.snake_head_pos[1] == food_pos[1]): 146 # если съели еду то задаем новое положение еды случайным 147 # образом и увеличивем score на один 148 food_pos = [random.randrange(1, screen_width/10)*10, 149 random.randrange(1, screen_height/10)*10] 150 score += 1 151 else: 152 # если не нашли еду, то убираем последний сегмент, 153 # если этого не сделать, то змея будет постоянно расти 154 self.snake_body.pop() 155 return score, food_pos 156 157 def draw_snake(self, play_surface, surface_color): 158 """Отображаем все сегменты змеи""" 159 play_surface.fill(surface_color) 160 for pos in self.snake_body: 161 # pygame.Rect(x,y, sizex, sizey) 162 pygame.draw.rect( 163 play_surface, self.snake_color, pygame.Rect( 164 pos[0], pos[1], 10, 10)) 165 166 def check_for_boundaries(self, game_over, screen_width, screen_height): 167 """Проверка, что столкунлись с концами экрана или сами с собой 168 (змея закольцевалась)""" 169 if any(( 170 self.snake_head_pos[0] > screen_width-10 171 or self.snake_head_pos[0] < 0, 172 self.snake_head_pos[1] > screen_height-10 173 or self.snake_head_pos[1] < 0 174 )): 175 game_over() 176 for block in self.snake_body[1:]: 177 # проверка на то, что первый элемент(голова) врезался в 178 # любой другой элемент змеи (закольцевались) 179 if (block[0] == self.snake_head_pos[0] and 180 block[1] == self.snake_head_pos[1]): 181 game_over() 182 183 184 class Food(): 185 def __init__(self, food_color, screen_width, screen_height): 186 """Инит еды""" 187 self.food_color = food_color 188 self.food_size_x = 10 189 self.food_size_y = 10 190 self.food_pos = [random.randrange(1, screen_width/10)*10, 191 random.randrange(1, screen_height/10)*10] 192 193 def draw_food(self, play_surface): 194 """Отображение еды""" 195 pygame.draw.rect( 196 play_surface, self.food_color, pygame.Rect( 197 self.food_pos[0], self.food_pos[1], 198 self.food_size_x, self.food_size_y)) 199 200 201 game = Game() 202 snake = Snake(game.green) 203 food = Food(game.brown, game.screen_width, game.screen_height) 204 205 game.init_and_check_for_errors() 206 game.set_surface_and_title() 207 208 while True: 209 snake.change_to = game.event_loop(snake.change_to) 210 211 snake.validate_direction_and_change() 212 snake.change_head_position() 213 game.score, food.food_pos = snake.snake_body_mechanism( 214 game.score, food.food_pos, game.screen_width, game.screen_height) 215 snake.draw_snake(game.play_surface, game.white) 216 217 food.draw_food(game.play_surface) 218 219 snake.check_for_boundaries( 220 game.game_over, game.screen_width, game.screen_height) 221 222 game.show_score() 223 game.refresh_screen()
Заключение
Данную игру можно улучшить, добавив изображения, музыку, еще ей не хватает настроек и меню, а также настроек сложности. Если вы хотите поделиться ею с друзьями, то, возможно, вы захотите скомпилировать ее в exe. Для этой цели можно использовать, например, pyinstaller:
pip install pyinstaller
pyinstaller --onefile snake_game.py
После чего в папке dist будет лежать бинарник.
Я сделал видео моей попытки поиграть: