Предыдущие части: Игровые уровни, Строим глазки, Таблица рекордов, Шрифт, Остановите музыку, Включите звук, Жизни и модальные окна, Подсчёт очков, Главное меню, Геймплей, Есть ли у него душа, Организация ввода, View, Визуализация поля, Загрузка уровня, INI-файл, Пишем Питона на Питоне!
Код для этой части находится в ветке setup на github. Вы можете смотреть там все файлы онлайн и также скачать зип-архив всей ветки.
Будем делать содержимое пункта Setup в главном меню. Как я уже говорил, Питон – не настолько сложная игра, чтобы наворачивать там всё, что мы наворотили. Но это тренировка в создании типичной игры с типичными задачами.
В настройки можно поместить следующее:
1. Разрешение экрана.
Хотя его можно определять автоматически, игрок может также выставить такое разрешение, какое он хочет. Плюс модуль pygame, как выяснилось, неправильно определяет разрешение экрана в Windows, где размер шрифта установлен на 150% (а это практически везде так, где экраны FullHD). Так что выставить размер экрана руками может быть полезно.
2. Полноэкранный режим
Игру можно стартовать в окне или в полноэкранном режиме.
3. Громкость музыки и звука
Эти настройки есть в модальном окне во время игры, но они не сохраняются. Можно сделать их в главном меню, чтобы игра запускалась с установленным уровнем громкости.
Ну и наверное хватит.
Все настройки, сделанные в главном меню, будут записываться в INI-файл. Его при желании можно будет исправить и руками.
Итак, для пункта настроек нужно сделать, как всегда, триаду из модели SetupModel, представления SetupView и контроллера SetupController.
Зачем плодить столько классов? У нас есть главное меню, игровой экран, экран следующего уровня, окно паузы, окно геймовера, экран результатов, экран финиша, экран ввода имени, экран настроек. 9 экранов, 9 режимов обработки данных. И каждому требуется три класса – модель, представление, контроллер. Итого 27 классов.
Ответ такой – мы целенаправленно применяем шаблон проектирования MVC. Да, получается много классов, зато:
- каждый класс выполняет чётко свою работу
- если бы над проектом работало несколько человек, они могли бы работать каждый над своим файлом, не мешая друг другу
- проект легко расширяется – мы можем добавлять сколько угодно новых игровых экранов, не испытывая трудностей
- за всё это время в игре не встретилось ни одного бага, связанного с проектированием интерфейса, то есть эта схема проста и надёжна
- некоторые классы можно использовать повторно, а не делать новые
В данный момент мы используем контроллер GameOverController в двух режимах и модель MenuModel везде, где есть меню. Так что SetupModel не понадобится. Можно и другие классы сделать более универсальными, чтобы один класс мог обслуживать разные режимы.
Но мы не будем делать их более универсальными, потому что в этой игре мы уже практически всё закончили.
Для работы с опциями нужно решить четыре задачи:
Задача 1. Представление данных в пункте меню
Для перебора опций внутри пункта меню нужен ещё один тип обработчика, добавим его как статическое свойство Menuitem.TYPE_LIST, то есть список. В качестве данных, используемых в этом пункте, передадим в конструктор MenuItem натурально список. Например, для пункта меню с разрешениями экрана это будет:
('800 x 600', '1024 x 768', '1280 x 720', '1366 x 768', '1920 x 1080')
А для пункта меню, включающего и выключающего полный экран:
('YES', 'NO')
То есть данному обработчику всё равно, что внутри списка. Он будет просто перебирать то, что в нём есть. Список будет перебираться стрелками влево и вправо, аналогично тому, как меняется громкость звука. Чтобы обработчик знал, какая позиция сейчас в списке текущая, её надо тоже передать в конструкторе MenuItem.
Проблема, однако, в том, что надо увязать элементы списка, которые представлены как обычные строки, с фактическими параметрами графики – шириной и высотой.
Доработаем компонент Graphics, чтобы он возвращал список экранных режимов. Этот список можно получить от модуля pygame, но мы для простоты поставим заглушку в виде готового списка. Метод Graphics.get_display_modes() вернёт нам кортеж из элементов-кортежей:
((800,600), (1024,768), (1280,720), (1366,768), (1920,1080))
Элементов столько же, и идут они в таком же порядке, как и элементы списка режимов меню. Таким образом, у нас есть два списка, между элементами которых есть соответствие. Зная номер элемента в одном из списков, мы можем найти соответствующий элемент в другом списке.
Осталось завести текущий номер, чтобы знать, в каком экранном режиме находится игра. Сделаем его свойством компонента: Graphics.display_mode_num.
Задача 2. Ширина меню
Класс MenuView умеет сам рассчитывать свою ширину, чтобы правильно располагаться на экране. Для этого он перебирает все пункты меню и выясняет, какой из них самый широкий. Но пункты, где есть перебор списка, требуют дополнительной проверки. Опции в списке тоже имеют свою ширину, поэтому сделан перебор ещё и всего списка опций, где тоже ищется самая длинная строка, затем её ширина складывается с общей.
Задача 3. Применение настроек из INI-файла
В INI-файле записаны ширина и высота экрана. При старте игры нужно выяснить, какому номеру графического режима они соответствуют, чтобы в меню этот режим отображался как текущий.
Для этого доработаем метод Graphics.set_display_mode(). Он будет сначала перебирать список режимов, чтобы убедиться, что заданный режим есть. Найдя его, он найдёт и его порядковый номер. А если не найдёт – то возьмёт первый режим из списка.
Наконец-то можно рисовать меню:
Добавим в метод MenuView.render_control() обработку типа TYPE_LIST. Она довольно проста: у нас есть список, и есть позиция в списке, нужно просто вывести соответствующую строку из этой позиции.
Обработкой же собственно изменений будет заниматься контроллер SetupController. Зная, какой пункт мы меняем, он будет производить соответствующие действия. Обработка громкости звука и музыки в этом контроллере просто скопирована из PauseController, где она уже написана. Это повод для объединения классов, но как я говорил выше, это делать мы уже не будем.
Контроллер имеет два локальных свойства: display_mode_num и fullscreen, то есть такие же, как у Graphics. Они нужны ему для того, чтобы отслеживать изменения, которые пользователь сделает в меню. Если при выборе пункта меню "APPLY AND SAVE CHANGES" эти свойства будут отличаться от одноимённых свойств Graphics, то контроллер установит новый экранный режим.
Задача 4. Сохранение параметров
Далее контроллер сохранит параметры графики и громкости звука в INI-файл. Для этого мы обновим соответствующие поля в компоненте Config и напишем ему метод save(), который инкапсулирует всю работу по сохранению.
Сам компонент Config, который мы сделали аж в самом первом выпуске, есть лишь более удобная оболочка над готовым питоновским компонентом configparser. У того есть метод write(), который сохраняет INI-файл. Так что задача метода Config.save() – просто перебросить нужные настройки в configparser и затем вызвать write().
Вот и всё. Можно сказать, что игра в целом закончена. Кроме того, я сделал новую музыку для финишного экрана.
Есть ещё поводы для доделок. Например, графика сейчас сделана под разрешение 1280x720, и в других разрешениях логотип съедет вбок, или по бокам от него будет пустота. Чтобы этого не происходило, в следующей части чуть причешем графику. А в 20-й, последней, части, доработаем дизайн уровней и геймплей.
Читайте дальше: Доделки