Пишем змейку на pygame | Блог python программиста
Изображение гика

Блог питониста

Пишем змейку на pygame

26 ноября 2017 г.

Есть такая библиотека для разработки двухмерных игр - 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 будет лежать бинарник.

Я сделал видео моей попытки поиграть:

Метки

pygame python
Если вам понравился пост, можете поделиться им в соцсетях: