Предыдущие части: Включите звук, Жизни и модальные окна, Подсчёт очков, Главное меню, Геймплей, Есть ли у него душа, Организация ввода, View, Визуализация поля, Загрузка уровня, INI-файл, Пишем Питона на Питоне!
Код для этой части находится в ветке options на github. Вы можете смотреть там все файлы онлайн и также скачать зип-архив всей ветки.
До сих пор меню состояли из обычных строчек. Но уже давно было задумано сделать опции, где можно использовать различные управляющие элементы, такие как переключатели и движки.
Можно сделать это очень просто через View (представление). У каждого меню есть своё представление. Например, у меню паузы есть представление PauseView. Внутри него мы можем в сущности делать всё что угодно, а именно: при рисовании меню мы знаем, что это конкретно меню паузы, а у него на первом месте стоит пункт "Звук", поэтому при рисовании первого пункта меню мы можем рядом с ним нарисовать переключатель для звука.
Это, во-первых, привязывает логику к неким фиксированным номерам пунктов. Впрочем, оно уже так и работает в контроллерах: скажем, при выборе пункта Play контроллер считает, что это первый по счёту элемент.
Во-вторых, в разных представлениях повторяется один и тот же код для рисования меню. А при добавлении управляющих элементов он станет повторяться еще сильнее.
Поэтому решим следующие задачи:
- Отвязать назначение пунктов меню от их порядковых номеров, чтобы пункты можно было свободно переставлять, добавлять, убирать и ничего не ломалось.
- Сделать общий класс представления для показа меню.
Чтобы решить первую задачу, сделаем пункты меню не просто строками, а объектами с набором свойств:
- Идентификатор: уникальный код, назначенный пункту меню. Представление и контроллер теперь будут "узнавать" его не по номеру, а по идентификатору.
- Имя файла: из какого файла загружать картинку для этого пункта меню.
- Тип: чем является пункт меню. Это или просто надпись, или переключатель, или движок, и т.д.
- Значение: если этот пункт меню является управляющим элементом, то какое значение в данный момент в нём хранится.
Создадим класс-модель MenuItem с вышеописанной структурой. В нём же объявим статические свойства-идентификаторы для всех пунктов меню и для их типов.
Затем изменим класс модели MenuModel. В конструктор будем передавать не список имен файлов, а список объектов класса MenuItem. Также метод модели get_item() теперь будет возвращать не картинку, а целый объект MenuItem.
Для второй задачи сделаем новый класс MenuView. Он будет выполнять общую функцию рисования меню, которая заключается в том, чтобы последовательно получать из модели пункты и рисовать их на экране. Это же представление будет отрисовывать и управляющие элементы. Например, если тип пункта меню равен "слайдер", то вместе с этим пунктом он нарисует слайдер для изменения того значения, которое хранится в этом пункте меню.
Представления MainMenuView, PauseView и GameOverView теперь освобождены от необходимости самостоятельно рисовать меню. Им надо только отнаследоваться от MenuView... но вот беда, PauseView уже отнаследован от ModalView (это модальное окно) и поэтому надо или наследовать ModalView от MenuView, или делать множественное наследование. Но и то и то показалось неудобным.
Поэтому в конструкторах этих представлений просто создаётся свойство menu_view, которому и присваивается объект класса MenuView. И мы имеем представление внутри представления.
Теперь PauseView в своём методе render() может вызвать super().render(), чтобы сначала отработал метод render() родителя (ModalView), затем нарисовать что-то своё, и затем нарисовать меню с помощью вызова self.menu_view.render().
Заодно изменим контроллеры, отнаследовав их от общего предка, который будет обрабатывать меню: MenuController. По стрелке вверх или вниз будет выбираться предыдущий или следующий пункт меню, по пробелу или вводу – вызываться метод process_item(). Этот метод в классе присутствует, но не реализован. Его реализация возлагается на потомков, ведь только они знают, что делать при выборе того или иного пункта меню.
В этом же классе обрабатываем нажатия стрелок влево и вправо. Это будет менять значение выбранного пункта меню в большую или меньшую сторону. Что за значение и как оно меняется, мы не в курсе и поэтому делаем ещё один метод process_control(), куда передаём текущий пункт меню и направление корректировки (1 или -1). Реализация этого метода также будет в потомках.
Теперь контроллеры MainMenuController, PauseController и GameOverController могут заниматься только конкретной обработкой пунктов меню, реализовав у себя метод process_item(). Контроллер PauseController пока единственный, где происходит изменение настроек, поэтому он также содержит метод process_control(). В нём он изменяет громкость звука или музыки.
Первоначальное значение громкости попадает в пункт меню тогда, когда оно создаётся в компоненте Game.
Читайте дальше: Шрифт
Читайте также:
- ООП в Python: особенности реализации
- Шаблон проектирования MVC