Мэтиз. Изучаем Python. Crash course2 n d e d i t i o na h a n d s o n, p r o j e c t b a s e d i n t r o d u c t i o n t o p r o g r a m m i n g
![]()
|
alien_invasion.py def _check_play_button(self, mouse_pos): """Запускает новую игру при нажатии кнопки Play.""" button_clicked = self.play_button.rect.collidepoint(mouse_pos) if button_clicked and not self.stats.game_active: # Сброс игровых настроек. self.settings.initialize_dynamic_settings() 302 Глава 14 • Ведение счета Игра Alien Invasion стала достаточно сложной и интересной. Каждый раз, когда игрок очищает экран, игра должна слегка ускориться, а ее сложность — слегка возрасти. Если сложность игры возрастает слишком быстро, уменьшите значение settings.speedup_scale , а если, наоборот, сложность недостаточна — слегка уве- личьте это значение. Найдите оптимальное значение, оценивая сложность игры за разумный промежуток времени. Первая пара флотов должна быть простой, не- сколько следующих — сложными, но возможными, а при последующих попытках сложность должна становиться практически безнадежной. УПРАЖНЕНИЯ 14.3. Учебная стрельба с нарастающей сложностью: начните с кода упражнения 14.2 (с. 299). Скорость мишени должна увеличиваться по ходу игры, а при нажатии игроком кнопки Play мишень должна возвращаться к исходной скорости. 14.4. Уровни сложности: создайте в Alien Invasion набор кнопок для выбора начальной сложности игры. Каждая кнопка должна присваивать атрибутам Settings значения, необхо- димые для создания различных уровней сложности. Подсчет очков Система подсчета очков позволит отслеживать счет игры в реальном времени; так- же на экране будет выводиться текущий рекорд, уровень и количество оставшихся кораблей. Счет игры также относится к игровой статистике, поэтому мы добавим атрибут score в класс GameStats : game_stats.py class GameStats(): def reset_stats(self): """Инициализирует статистику, изменяющуюся в ходе игры.""" self.ships_left = self.ai_settings.ship_limit self.score = 0 Чтобы счет сбрасывался при запуске новой игры, мы инициализируем score в reset_stats() вместо __init__() Вывод счета Чтобы вывести счет на экран, мы сначала создаем новый класс Scoreboard . Пока этот класс ограничивается выводом текущего счета, но мы используем его для вы- вода рекордного счета, уровня и количества оставшихся кораблей. Ниже приведена первая часть класса; сохраните ее под именем scoreboard .py : Подсчет очков 303 scoreboard.py import pygame.font class Scoreboard(): """Класс для вывода игровой информации.""" ❶ def __init__(self, ai_game): """Инициализирует атрибуты подсчета очков.""" self.screen = ai_game.screen self.screen_rect = self.screen.get_rect() self.settings = ai_game.settings self.stats = ai_game.stats # Настройки шрифта для вывода счета. ❷ self.text_color = (30, 30, 30) ❸ self.font = pygame.font.SysFont(None, 48) # Подготовка исходного изображения. ❹ self.prep_score() Так как Scoreboard выводит текст на экран, код начинается с импортирования модуля pygame.font . Затем __init__() передается параметр ai_game для обраще- ния к объектам settings , screen и stats , чтобы класс мог выводить информацию об отслеживаемых показателях . Далее назначается цвет текста и создается экземпляр объекта шрифта . Чтобы преобразовать выводимый текст в изображение, мы вызываем метод prep_ score() , который определяется следующим образом: scoreboard.py def prep_score(self): """Преобразует текущий счет в графическое изображение.""" ❶ score_str = str(self.stats.score) ❷ self.score_image = self.font.render(score_str, True, self.text_color, self.settings.bg_color) # Вывод счета в правой верхней части экрана. ❸ self.score_rect = self.score_image.get_rect() ❹ self.score_rect.right = self.screen_rect.right - 20 ❺ self.score_rect.top = 20 В методе prep_score() числовое значение stats.score преобразуется в строку ; эта строка передается методу render() , создающему изображение . Чтобы счет был хорошо виден на экране, render() передается цвет фона и цвет текста. Счет размещается в правой верхней части экрана и расширяется влево с ростом значения и ширины числа. Чтобы счет всегда оставался выровненным по правой стороне, мы создаем прямоугольник rect с именем score_rect и смещаем его правую сторону на 20 пикселов от правого края экрана . Затем верхняя сторона прямоугольника смещается на 20 пикселов вниз от верхнего края экрана . 304 Глава 14 • Ведение счета Остается создать метод show_score() для вывода построенного графического изо- бражения: scoreboard.py def show_score(self): """Выводит счет на экран.""" self.screen.blit(self.score_image, self.score_rect) Метод выводит счет на экран в позиции, определяемой score_rect Создание экземпляра Scoreboard Чтобы вывести счет, мы создадим в AlienInvasion экземпляр Scoreboard . Начнем с обновления команд импортирования: alien_invasion.py from game_stats import GameStats from scoreboard import Scoreboard Затем создадим экземпляр Scoreboard в __init__() : alien_invasion.py def __init__(self): pygame.display.set_caption("ALien Invasion") # Создание экземпляров для хранения статистики # и панели результатов. self.stats = GameStats(self) self.sb = Scoreboard(self) Затем панель результатов выводится на экран в _update_screen() : alien_invasion.py def _update_screen(self): self.aliens.draw(self.screen) # Вывод информации о счете. self.sb.show_score() # Кнопка Play отображается в том случае, если игра неактивна. Метод show_score() вызывается непосредственно перед отображением кнопки Play Подсчет очков 305 Если запустить Alien Invasion сейчас, в правом верхнем углу экрана отображается счет 0. (Пока мы просто хотим убедиться в том, что счет отображается в нужном месте, прежде чем заниматься дальнейшей доработкой системы подсчета очков.) На рис. 14.2 изображено окно игры перед ее началом. Рис. 14.2. Счет отображается в правом верхнем углу экрана А теперь нужно организовать начисление очков за каждого пришельца! Обновление счета при уничтожении пришельцев Чтобы на экране выводился оперативно обновляемый счет, мы будем обновлять значение stats.score при каждом попадании в пришельца, а затем вызывать prep_score() для обновления изображения счета. Но сначала нужно определить, сколько очков игрок будет получать за каждого пришельца: settings.py def initialize_dynamic_settings(self): # Подсчет очков self.alien_points = 50 Стоимость каждого пришельца в очках будет увеличиваться по ходу игры. Что- бы значение сбрасывалось в начале каждой новой игры, мы задаем значение в initialize_dynamic_settings() 306 Глава 14 • Ведение счета Счет будет обновляться за каждого сбитого пришельца в _check_bullet_alien_ collisions() : alien_invasion.py def _check_bullet_alien_collisions(self): """Обработка коллизий снарядов с пришельцами.""" # Удаление снарядов и пришельцев, участвующих в коллизиях. collisions = pygame.sprite.groupcollide( self.bullets, self.aliens, True, True) if collisions: self.stats.score += self.settings.alien_points self.sb.prep_score() При попадании снаряда в пришельца Pygame возвращает словарь collisions Программа проверяет, существует ли словарь, и если существует, стоимость при- шельца добавляется к счету. Затем вызов prep_score() создает новое изображение для обновленного счета. Теперь во время игры вы сможете набирать очки! Сброс счета В текущей версии счет обновляется только после попадания в пришельца; в ос- новном такой подход работает нормально. Но старый счет выводится и после по- падания в первого пришельца в новой игре. Проблема решается инициализацией счета при создании новой игры: alien_invasion.py def _check_play_button(self, mouse_pos): if button_clicked and not self.stats.game_active: # сброс игровой статистики. self.stats.reset_stats() self.stats.game_active = True self.sb.prep_score() Метод prep_score() вызывается при сбросе игровой статистики в начале новой игры. Счет, выводимый на экран, обнуляется. Начисление очков за все попадания В том виде, в каком написан код, некоторые пришельцы будут пропускаться при подсчете. Например, если два снаряда попадают в пришельцев во время одного прохода цикла или если вы создадите широкий «снаряд» для поражения несколь- Подсчет очков 307 ких пришельцев одновременно, игрок получит очки только за одного подбитого пришельца. Чтобы устранить этот недостаток, нужно доработать механизм обна- ружения коллизий между снарядами и пришельцами. В коде _check_bullet_alien_collisions() любой снаряд, столкнувшийся с при- шельцем, становится ключом словаря collisions . С каждым снарядом связывается значение — список пришельцев, участвующих в коллизии. Переберем словарь collisions и убедимся в том, что очки начисляются за каждого подбитого при- шельца: alien_invasion.py def _check_bullet_alien_collisions(self): if collisions: ❶ for aliens in collisions.values(): self.stats.score += self.settings.alien_points * len(aliens) self.sb.prep_score() Если словарь collisions был определен, программа перебирает все значения в словаре . Вспомните, что каждое значение представляет собой список при- шельцев, в которых попал один снаряд. Стоимость каждого пришельца умножается на количество пришельцев в списке, а результат прибавляется к текущему счету. Чтобы протестировать эту систему, увеличьте ширину снаряда до 300 пикселов и убедитесь в том, что игра начисляет очки за каждого пришельца, в которого попал этот большой снаряд; затем верните ширину снаряда к нормальному состоянию. Увеличение стоимости пришельцев Так как с каждым достижением нового уровня игра становится более сложной, за пришельцев на этих уровнях следует давать больше очков. Чтобы реализовать эту функциональность, мы добавим код, увеличивающий стоимость пришельцев при возрастании скорости игры: settings.py class Settings(): """Класс для хранения всех настроек игры Alien Invasion.""" def __init__(self): # Темп ускорения игры self.speedup_scale = 1.1 # Темп роста стоимости пришельцев ❶ self.score_scale = 1.5 self.initialize_dynamic_settings() def initialize_dynamic_settings(self): 308 Глава 14 • Ведение счета def increase_speed(self): """Увеличивает настройки скорости и стоимость пришельцев.""" self.ship_speed_factor *= self.speedup_scale self.bullet_speed_factor *= self.speedup_scale self.alien_speed_factor *= self.speedup_scale ❷ self.alien_points = int(self.alien_points * self.score_scale) В программе определяется коэффициент прироста начисляемых очков, он на- зывается score_scale . С небольшим увеличением скорости (1,1) игра быстро усложняется, но чтобы увидеть заметную разницу в очках, необходимо изменять стоимость пришельцев в большем темпе (1,5). После увеличения скорости игры стоимость каждого попадания также увеличивается . Чтобы счет возрастал на целое количество очков, в программе используется функция int() Чтобы увидеть стоимость каждого пришельца, добавьте в метод increase_speed() в классе Settings команду print : settings.py def increase_speed(self): self.alien_points = int(self.alien_points * self.score_scale) print(self.alien_points) Новое значение должно выводиться в терминальном окне каждый раз, когда игрок переходит на новый уровень. ПРИМЕЧАНИЕ Убедившись, что стоимость пришельцев действительно возрастает, не забудьте удалить команду print; в противном случае лишний вывод повлияет на бы- стродействие игры и будет отвлекать игрока . Округление счета В большинстве аркадных «стрелялок» счет ведется значениями, кратными 10, и мы воспользуемся этой схемой в своей игре. Давайте отформатируем счет так, чтобы в больших числах группы разрядов разделялись запятыми. Изменения вносятся в классе Scoreboard : scoreboard.py def prep_score(self): """Преобразует текущий счет в графическое изображение.""" ❶ rounded_score = round(self.stats.score, -1) ❷ score_str = "{:,}".format(rounded_score) self.score_image = self.font.render(score_str, True, self.text_color, self.ai_settings.bg_color) Функция round() обычно округляет дробное число до заданного количества зна- ков, переданного во втором аргументе. Но если во втором аргументе передается Подсчет очков 309 отрицательное число, round() округляет значение до ближайших десятков, сотен, тысяч и т. д. Код приказывает Python округлить значение stats.score до десят- ков и сохранить его в rounded_score В точке директива форматирования строки приказывает Python вставить запя- тые при преобразовании числового значения в строку — например, чтобы вместо 1000000 выводилась строка 1,000,000. Теперь при запуске игры всегда будет ото- бражаться аккуратно отформатированный, округленный счет (рис. 14.3). Рис. 14.3. Округленный счет с разделителями разрядов Рекорды Каждый игрок желает превзойти предыдущий рекорд игры, поэтому мы будем от- слеживать и выводить рекорды, чтобы у игрока была ясная цель. Рекорды будут храниться в классе GameStats : game_stats.py def __init__(self, ai_game): # Рекорд не должен сбрасываться. self.high_score = 0 Так как рекорд не должен сбрасываться при повторном запуске, значение high_ score инициализируется в __init__() , а не в reset_stats() 310 Глава 14 • Ведение счета Теперь изменим класс Scoreboard для отображения рекорда. Начнем с метода __init__() : scoreboard.py def __init__(self, ai_game): # Подготовка изображений счетов. self.prep_score() ❶ self.prep_high_score() Рекорд должен отображаться отдельно от текущего счета, поэтому для подготовки его изображения понадобится новый метод prep_high_score() : scoreboard.py def prep_high_score(self): """Преобразует рекордный счет в графическое изображение.""" ❶ high_score = round(self.stats.high_score, -1) high_score_str = "{:,}".format(high_score) ❷ self.high_score_image = self.font.render(high_score_str, True, self.text_color, self.ai_settings.bg_color) # Рекорд выравнивается по центру верхней стороны. self.high_score_rect = self.high_score_image.get_rect() ❸ self.high_score_rect.centerx = self.screen_rect.centerx ❹ self.high_score_rect.top = self.score_rect.top Рекорд округляется до десятков и форматируется с запятыми . Затем для рекорда строится графическое изображение , выполняется горизонтальное выравнивание прямоугольника по центру экрана , а атрибут top прямоугольника приводится в соответствие с верхней стороной изображения счета . Теперь метод show_score() выводит текущий счет в правом верхнем углу, а ре- корд — в центре верхней стороны: scoreboard.py def show_score(self): """Выводит счет на экран.""" self.screen.blit(self.score_image, self.score_rect) self.screen.blit(self.high_score_image, self.high_score_rect) Для обновления рекорда в Scoreboard добавляется новая функция check_high_ score() : scoreboard.py def check_high_score(self): """Проверяет, появился ли новый рекорд.""" if self.stats.score > self.stats.high_score: self.stats.high_score = self.stats.score self.prep_high_score() Подсчет очков 311 Метод check_high_score() сравнивает текущий счет с рекордом. Если текущий счет выше, мы обновляем значение high_score и вызываем prep_high_score() для обновления изображения рекорда. Метод check_high_score() должен вызываться при каждом попадании в пришельца после обновления счета в _check_bullet_alien_collisions() : alien_invasion.py def check_bullet_alien_collisions(self): if collisions: for aliens in collisions.values(): self.stats.score += self.settings.alien_points * len(aliens) self.sb.prep_score() self.check_high_score() Метод check_high_score() должен вызываться только в том случае, если словарь collisions присутствует, причем вызов выполняется после обновления счета для всех подбитых пришельцев. Когда вы впервые играете в Alien Invasion, текущий счет одновременно будет наивысшим, поэтому он будет отображаться и как текущий счет, и как рекорд. Но в начале второй игры ваш предыдущий рекорд должен отображаться в середине, а текущий счет справа, как показано на рис. 14.4. Рис. 14.4. Рекордный счет выводится в середине экрана 312 Глава 14 • Ведение счета Вывод уровня Чтобы в игре выводился текущий уровень, сначала в класс GameStats следует вклю- чить атрибут для его представления. Чтобы уровень сбрасывался в начале каждой игры, инициализируйте его в reset_stats() : game_stats.py def reset_stats(self): """Инициализирует статистику, изменяющуюся в ходе игры.""" self.ships_left = self.ai_settings.ship_limit self.score = 0 self.level = 1 Чтобы класс Scoreboard выводил текущий уровень, мы вызываем новый метод prep_level() из __init__() : scoreboard.py def __init__(self, ai_game): self.prep_high_score() self.prep_level() Метод prep_level() выглядит так: scoreboard.py def prep_level(self): """Преобразует уровень в графическое изображение.""" level_str = str(self.stats.level) ❶ self.level_image = self.font.render(level_str, True, self.text_color, self.settings.bg_color) # Уровень выводится под текущим счетом. self.level_rect = self.level_image.get_rect() ❷ self.level_rect.right = self.score_rect.right ❸ self.level_rect.top = self.score_rect.bottom + 10 Метод prep_level() создает изображение на базе значения, хранящегося в stats. level , и приводит атрибут right изображения в соответствие с атрибутом right счета . Затем атрибут top сдвигается на 10 пикселов ниже нижнего края изображения текущего счета, чтобы между счетом и уровнем оставался пустой интервал . В метод show_score() также необходимо внести изменения: |