Найти тему
ZDG

Пишем Питона на Питоне #10: Подсчёт очков и первый баг

Предыдущие части: Главное меню, Геймплей, Есть ли у него душа, Организация ввода, View, Визуализация поля, Загрузка уровня, INI-файл, Пишем Питона на Питоне!

Содержимое этой части находится в ветке score-stats на github. Вы можете смотреть там все файлы онлайн и также скачать зип-архив всего проекта.

Подсчёт очков вроде бы сделать несложно. Завести переменную, допустим, score, и просто прибавлять к ней какое-то число каждый раз, когда питон съест яблоко.

Но кроме того, очки нужно ещё и отображать на экране. Тоже просто? Не совсем.

В качестве шрифта я предпочитаю использовать картинки. Это даёт несколько преимуществ:

  • Не нужно, чтобы на компьютере был установлен какой-то шрифт.
  • Шрифтовые библиотеки не всегда удобно и адекватно отрисовывают шрифты. А картинки будут выглядеть везде одинаково.
  • Можно нарисовать любой, совершенно уникальный шрифт, подходящий только для этой игры.
  • Картинку можно раскрасить как угодно, чего не сделаешь с шрифтом.
  • Картинка рисуется быстрее.

Недостатки тоже есть. Вместо простого вывода строки теперь нужно перебирать её посимвольно и выводить на экран картинку за картинкой. И поменять цвет картинки нельзя. Чтобы был шрифт другого цвета, нужен другой набор картинок, либо при рисовании нужно применять определённые фильтры.

Также это трудоёмко. По-хорошему свою картинку нужно нарисовать для каждого символа алфавита, который используется в игре. Но у нас в главном меню надписи сделаны целиком как картинки, и поэтому пока что реально нужны только цифры, чтобы составлять из них разные числа.

Я не буду их рисовать, а просто напишу шрифтом в Xara X и буду считать, что я их типа нарисовал:

иллюстрация увеличена для наглядности
иллюстрация увеличена для наглядности

В чём заключается подготовка:

  1. Каждая цифра это отдельный шрифтовой объект, чтобы их можно было сдвигать или раздвигать.
  2. Горизонтальное положение каждой цифры (левая сторона) приведено к целому числу.
  3. Ширина цифры также округлена до целого числа. Это чуть-чуть сжимает или расширяет картинку, всего на долю пиксела, но зато теперь по ширине она вписывается ровно в прямоугольник.
  4. Для всех цифр я нарисовал обрамляющие прямоугольники, которые задают координаты и ширину цифр (высота одна и та же у всех). Для наглядности я сделал их цветными. В финальном варианте они будут прозрачными.

Теперь я просто выделяю каждый прямоугольник в Xara X и смотрю его смещение и ширину. У меня получилось:

0: 0, 25
1: 25, 15
2: 40, 22
3: 62, 22
и т.д.

Так как выводом графики у нас заведует компонент Graphics, то добавим в него нужные свойства и методы для вывода текста.

В свойстве img_font будет храниться вышеописанная картинка, где содержатся 10 цифр, а в свойстве font_map будет словарь (ассоциативный массив), где каждому символу будут поставлены в соответствие его смещение и ширина.

Метод draw_text(s, x, y) будет работать так: получив на вход строку s, он пройдётся по каждому символу. Для каждого символа он найдёт соответствие в font_map. Зная, где находится символ в картинке, метод сможет отрисовать на экране в позиции (x, y) ту часть картинки, которая соответствует этому символу. Да, картинка будет рисоваться не вся, а только частично, только нужное её место. После этого, зная ширину символа, метод прибавит к текущей координате x эту ширину и будет готов рисовать следующий символ.

Чтобы вывести число, нужно превратить его в строку. Для этого просто используем стандартную функцию str().

Если вы уже поднаторели в выявлении зависимостей, то можете заметить, что я поступил очень опрометчиво. В классе Graphics находится фиксированный статический словарь font_map, который рассчитан только на конкретную картинку. Если подсунуть другую картинку (где символы идут не в том порядке или отличаются размерами), этот словарь будет бесполезен. Загрузка картинки с шрифтом также происходит из файла с фиксированным именем. Так что мы создали аж две зависимости. Чтобы это исправить, нужно как минимум связать словарь с конкретным файлом изображения. То есть загружать их в паре или придумывать какой-то особый формат или класс хранения. Просто обратим на это внимание, но ничего делать не будем. Сейчас наша цель – получить MVP (Minimum Viable Product), минимально рабочий продукт, а то, что я сделал, называется "прибить гвоздями".

Статистика будет располагаться сверху и иметь такой вид:

-2

На данный момент я просто экспортирую всю эту полоску как есть, но без цифр. Она будет выводиться на экран, а цифры будут выводиться поверх неё:

-3

Пользуясь случаем, доработаем представление GameView. Наш игровой уровень выводился в левом верхнем углу экрана. Учитывая высоту верхней полоски, мы можем расположить уровень в середине свободного пространства, а также задать для него такой масштаб, чтобы на любом размере экрана он автоматически заполнял всё доступное место.

Для этого надо знать размеры уровня, и мы их в принципе уже знаем. Высота уровня это длина массива строк, который передаётся в модель Level при загрузке уровня, а ширина уровня уже и так вычислялась как свойство line_length для правильной адресации в матрице. Но количество строк раньше не запоминалось, поэтому добавим свойство height, а чтобы всё было аккуратно, то исправим line_length на width.

Продолжим доработку представления GameView, которое занимается отрисовкой уровня. В его задачи теперь входит: получить размеры экрана, получить размеры уровня, вычислить положение уровня на экране и размер одной клетки так, чтобы уровень по максимуму заполнил экран. Так как уровень после загрузки не меняется, все необходимые вычисления я сделал в конструкторе. Мы берем ширину экрана и делим на ширину уровня, чтобы получить размер клетки по вертикали. Потом высоту экрана (минус верхняя полоска) делим на высоту уровня, чтобы получить размер клетки по вертикали. Какой из этих размеров меньше, такой и оставляем (свойство cell_size). Затем уже используя этот размер, вычисляем положение уровня по центру (свойства level_x, level_y). Теперь всё выглядит гораздо лучше:

-4

На сегодня осталось сделать только вывод набранных очков и длины. Очевидно, длина питона хранится в классе Python. А где будем хранить переменную score? В питоне? В контроллере? В представлении?

Напомню, что у нас есть объект класса Game, который существует всё время, пока остальные объекты (контроллеры, модели и представления) меняются. Поэтому было бы логично хранить score в Game до самого конца игры. Я добавил туда это свойство.

Не будем пока сильно мудрствовать, и пусть на каждом шаге к score прибавляется 1, а когда питон съедает яблоко, то прибавляется 5. Этим будет заведовать GameController. Но так как ему не положено самому что-то рисовать, он будет просто записывать эту информацию (а также длину питона) в свойства представления GameView, соответственно, score и length (как будто это мини-модели). А уж оно выведет текст там, где надо:

-5

Что ж, игра становится всё более похожей на игру?

А что за баг?

Конечно, в вашем проекте рано или поздно появятся баги, и вот они появились. Баг обнаружился, когда я пытался отцентровать уровень. Он почему-то ставился немного вбок, как будто у него справа должна быть лишняя клетка, но её не было. В конце концов выяснилось, что строки из текстового файла, которые передаются в конструктор уровня, имеют в конце символы переноса строки, известные как "\n\r", и эти символы учитывались, когда уровень определял свою максимальную ширину. Пришлось добавить инструкцию:

line = line.rstrip()

в конструктор класса Level, чтобы удалить эти символы.

В следующей части мы добавим жизни и финальные экраны.

Следующая часть: Жизни и модальные окна

Читайте также: