Создание мобильных приложений на Python: интересное занятие для детей
Сегодня язык программирования Python — один из самых востребованных в мире. Несмотря на свой приличный возраст — почти 22 года — он не теряет актуальности и используется все чаще. За последние годы индекс популярности языков программирования TIOBE, в составлении которого участвуют разработчики со всего мира, трижды провозгласил его языком года. В этой статье поговорим о том, чем же он так хорош, и какие мобильные приложения на Python смогут попробовать создать начинающие программисты.
Что такое Python
Если коротко, то в конце 80-х годов прошлого века голландский математик и программист Гвидо ван Россум начал создавать новый текстовый язык программирования. Основным его планом было сделать синтаксис своего детища по-настоящему простым и не громоздким, и все получилось. Там, где в другом языке нужно использовать семь строчек кода, в Питоне можно обойтись двумя. Приведем простейший пример.
Как напечатать «Hello, World» на Python:
print «Hello World» # Python < 3.0 print(«Hello World«) # Python ≥ 3.0
И на другом популярном языке Java
public class Main < public static void main (String[] args) < System.out.println(«Hello World«); >>
Эта особенность языка Питон позволяет легче изучать его, увеличивает производительность, существенно упрощает и написание, и чтение кода. Ну и, конечно, делает его таким популярным.
Кроме того, разработчики любят Python за возможность экономить усилия и время. Часть модулей при создании программ можно не писать самостоятельно, а взять уже готовые в одной из обширных библиотек. Еще одной важной особенностью считается то, что этот язык поддерживает базы Big Data. Ведь именно на них основано большинство современных и актуальных разработок, технических и научных.
На Python сегодня пишут все: от чат-ботов, мобильных и веб-приложений до сложнейших систем по изучению и анализу больших данных и обучению искусственного интеллекта. Все говорит о том, что этим языком будут пользоваться, как минимум, ближайшие несколько десятилетий.
Поэтому сегодня так много родителей в качестве внеклассных занятий выбирают курсы обучения подростков программированию на Python. В первую очередь, это интересно, во вторую, хотя для многих это стоит на первом месте, полезно. Профессия питон-разработчика не только перспективная и востребованная, но и хорошо оплачиваемая. Так что, такие уроки программирования для детей — инвестиция в их будущее.
Создание приложений на Python
Недостатком языка программирования Питон в том, что в нем нет встроенных функций мобильного интерфейса. Ведь когда его придумали, и речи не шло о написании программ для смартфонов. Поэтому традиционно Python считается языком программирования для веб-разработки.
Однако со временем «питонисты» смогли адаптировать свой любимый язык и под создание мобильных приложений на Python. Для этого они создали несколько фреймворков, то есть, IT-моделей, которые позволяют заниматься мобильной разработкой в среде Питон. Самые популярные из них — Kivy и Beeware.
Kivy — это библиотека с открытым исходным кодом. Чаще всего ее используют для написания кроссплатформенных приложений, которые будут одинаково выглядеть и работать везде: на iOS и Android, Windows и macOS.
BeeWare считается более гибким, поскольку позволяет создавать приложения с разным пользовательским интерфейсом (UI) для различных платформ. Поэтому чаще разработчики отдают предпочтение ему.
Сегодня с использованием Python поддерживается работа таких популярных приложений, как, например, Pinterest, который изначально писался именно на этом языке. На нем же написан Panda3D — движок, на котором создаются практически все мобильные игры от Walt Disney. Да что там, даже мобильные версии Google и YouTube уже не могут обойтись без Питона. Так что обучение созданию мобильных приложений для детей, да и для взрослых, изучающих Python — практически маст хэв. Тем более, вам вовсе не обязательно начинать с чего-то сложного.
Простые мобильные игры
Обучение созданию мобильных приложений для подростков проще всего начать с игр. Приведем несколько примеров.
Угадай число — игра, в которой программа загадывает случайное число в определенном диапазоне, а игрок или игроки должны его отгадать.
Чтобы ее написать, нужно использовать генератор случайных чисел. Как научить программу выбирать число из списка, можно посмотреть в бесплатном видео-уроке по Python.
Случайное число в Python: видео-урок для детей
Кроме него для написания кода используются переменные и целые числа, функция вывода на экран, операторы if/else и цикл while, позволяющий повторять одни и те же действия, пока условие, которое он проверяет, истинно.
Можно попробовать сделать и более продвинутый вариант этой игры — Виселицу. В ней игрока будет ограниченное количество попыток отгадать уже не цифры, а буквы в заданном программой слове.
Список слов можно взять в одном из словарей библиотеки Питон, например, в Sowpods. Помимо этого, пригодится опыт работы с циклом while, освежить знания о котором можно, посмотрев видео-урок ниже.
Видео-урок по работе с циклами на Python
Также советуем вспомнить, генератор случайных чисел, обработку ввода и вывод, условия if/else, работу со строками и со списками:
Видео-урок по работе со списками на Python
Камень, ножницы, бумага – игра, рассказывать о правилах которой нет смысла, все и так их знают. Просто играете вы в нее не с кем-то другим, а с приложением.
Здесь вам тоже нужно будет знать, как в Python создается генератор случайных чисел, уметь обрабатывать ввод данных и выводить их на экран, использовать все тот же цикл while. Ну и не стоит забывать про условные конструкции if/else, подробнее о которых можно узнать в этом видеоуроке по обучению детей программированию на Python.
Условные конструкции: видео-урок по программированию на Python для детей
Текстовый квест – в самом простом его варианте игрок переходит из локации в локацию, получая их описание. Локации зависят от вашей фантазии, это могут быть как разные комнаты, так и разные миры.
Чтобы написать эту игру, требуются те же знания, что и в предыдущем случае. Однако здесь все усложняется тем, что необходимо будет отслеживать перемещения игрока, создавать стены, через которые он не сможет пройти и двери, которые нужно будет открывать.
Правила игры в крестики нолики тоже известны многим – на поле размером 3х3 квадрата каждый из игроков рисует свою фигуру, крестик или нолик. Выигрывает тот, кто первым сможет поставить три фигуры в ряд по горизонтали, вертикали или диагонали.
Здесь уже понадобится использовать графику, например, из библиотеки PyGame. Но основная сложность не в этом, а в программировании ходов приложения, для чего вам придется учесть максимально возможное количество вариантов развития игровых событий.
Полезные приложения и виджеты на Python
Чтобы обучение детей программированию на Python не сводилось только к созданию игр, поговорим о программах, которые могут пригодиться им на практике, например, при произведении вычислений, поиске информации, планировании своего времени и так далее.
Сама простая из них называется алгоритмом двоичного поиска и позволяет проверять, есть ли запрошенная пользователем информация в известном списке данных. Для этого в программу вводится тот самый список чисел, слов и так далее. Пользователь вводит запрос, программа проводит его сверку со списком и выводит результат.
Начинающему разработчику здесь нужны будут знания о создании циклов, умение программировать ввод и вывод данных, а также условия if/else.
Калькулятор – может быть проще или сложнее, с расширенным функционалом, где к возможностям ввода чисел и проведения простых арифметических действий, сложения, вычитания, умножения и деления, добавляется возведение в степень, извлечение корня или, к примеру, возможность хранить в памяти программы результаты вычислений. Все здесь зависит только от потребностей при использовании.
Помочь в создании графического интерфейса могут данные библиотек Tkinter и PyQt.
Еще одна суперполезная вещь – будильник, особенно, если вам не нравится тот, что уже есть на смартфоне. Вы можете запрограммировать его так, чтобы получать в назначенное время уведомления с любым содержимым: звуками, текстом, картинками или видео.
Следующая полезная программа, связанная со временем – таймер обратного отсчета до указанного вами события, который будет виден на основном экране или экране блокировки, и предупредит сигналом о наступлении того самого события.
Всем известна программа Блокнот, в которой можно вводить и редактировать текст, а также открывать текстовые фйлы практически любого формата. Написать похожую для мобильного довольно просто.
Чтобы реализовать весь ее несложный функционал, можно использовать QTextEdit.
Обучение программированию на Python для детей
Сегодня, когда всюду действует правило mobile first, то есть, большинство интерфейсов сначала создаются для мобильных браузеров или приложений, мобильный разработчик – отличная профессия для настоящего и будущего.
Научиться созданию приложений на Python можно и самостоятельно, информации на просторах интернета для этого достаочно. Но чаще всего дети и подростки еще не могут сами правильно построить программу, систематизировать информацию, найти ее в нужном объеме. Поэтому начинать изучение Python лучше на специальных курсах, где ребята могут заниматься уже с 9-10 лет. Чтобы заниматься, достаточно компьютера и стабильно работающего интернета, ведь уроки программирования для детей проходят онлайн раз в неделю, индивидуально или в группе. За три месяца они освоят основы программирования на Питоне и дальше смогут развиваться в интересном им направлении: создавать игры, писать интересные и полезные приложения, придумывать что-то принципиально новое.
Кстати, нынешнее поколение senior-разработчиков и руководителей команд в 20-21 год выросло из детей, которые пришли в разработку в раннем возрасте. Многие из них получают высшее образование и параллельно работают в крупных компаниях, создают программы для портфолио, участвуют в хакатонах (соревнованиях для программистов) для получения необходимого опыта.
Уже сейчас вы можете обеспечить детям интересное будущее с любимой и перспективной работой. И чем раньше начнется обучение подростков программированию, тем быстрее они придут к своим первым успехам.
Пишем «Змейку» под Android на Python и Kivy
В этой статье мы напишем классическую «Змейку» на Python с помощью инструмента для создания GUI Kivy.
Знакомимся с Kivy
Kivy — это популярный инструмент для создания пользовательских интерфейсов, который отлично подходит для разработки приложений и несложных игр. Одним из основных достоинств Kivy является портируемость — возможность безболезненного переноса ваших проектов с одной платформы на другую. Наша «Змейка» будет работать на платформе Android.
Kivy эффективно использует Cython — язык программирования, сочетающий в себе оптимизированность C++ и синтаксис Python — что положительно сказывается на производительности. Также Kivy активно использует GPU для графических процессов, освобождая CPU для других вычислений.
Рекомендуемые ресурсы для начала работы с Kivy:
- официальная документация;
- Wiki по Kivy;
- примеры готовых проектов.
Устанавливаем Kivy
Зависимости
Прим. перев. Код проверен на Ubuntu 16.04, Cython 0.25, Pygame 1.9.4.dev0, Buildozer 0.33, Kyvi 1.10.
Для правильной работы Kivy нам требуется три основных пакета: Cython, pygame и python-dev. Если вы используете Ubuntu, вам также может понадобиться библиотека gstreamer, которая используется для поддержки некоторых видеовозможностей фреймворка.
sudo pip install cython
Устанавливаем зависимости pygame:
sudo apt-get build-dep python-pygame sudo apt-get install python-dev build-essential
sudo pip install hg+http://bitbucket.org/pygame/pygame
sudo apt-get install gstreamer1.0-libav
Добавляем репозиторий Kivy:
sudo add-apt-repository ppa:kivy-team/kivy sudo apt-get update
sudo apt-get install python-kivy
Buildozer
Этот пакет нам понадобится для упрощения процесса установки нашего Android-приложения:
sudo pip install buildozer
Нам также понадобится Java JDK. И если вы используете 64-битную систему, вам понадобятся 32-битные версии зависимостей.
Устанавливаем Java JDK:
sudo apt-get install openjdk-7-jdk
Устанавливаем 32-битные зависимости:
sudo dpkg --add-architecture i386 sudo apt-get update sudo apt-get install libncurses5:i386 libstdc++6:i386 zlib1g:i386
Оно работает?
Прежде чем начать писать нашу «Змейку», давайте проверим, правильно ли у нас все установилось. Иначе в дальнейшем может обнаружиться, что проект не компилируется из-за какого-нибудь недостающего пакета.
Для проверки напишем старый добрый «Hello, world!».
Приступим к созданию проекта. Нужно перейти в рабочую папку и выполнить команду:
buildozer init
Теперь откроем файл с расширением .spec в любом текстовом редакторе и изменим следующие строки:
- имя нашего приложения title = Hello World ;
- название пакета package.name = helloworldapp ;
- домен пакета (нужен для android/ios сборки) package.domain = org.helloworldapp ;
- закомментируйте эти строки, если они ещё не закомментированы:
# version.regex = __version__ = ['"](.*)['"] # version.filename = %(source.dir)s/main.py
- строка version = 1.0.0 должна быть раскомментированной.
Создайте файл main.py и добавьте в него следующий код:
import kivy kivy.require('1.8.0') # Ваша версия может отличаться from kivy.app import App from kivy.uix.button import Button class DummyApp(App): def build(self): return Button(text="Hello World") if __name__ == '__main__': DummyApp().run()
Теперь все готово к сборке. Вернемся к терминалу.
buildozer android debug # Эта команда создает apk файл в папке ./bin buildozer android debug deploy # Если вы хотите установить apk непосредственно на ваше устройство
Примечание В случае возникновения каких-либо ошибок установите значение log_level = 2 в файле buildozer.spec. Это даст более развернутое описание ошибки. Теперь мы точно готовы приступить к написанию «Змейки».
Прим. перев. Обратите внимание, что в проекте используется один файл и в статье разбираются лишь куски кода из этого файла. Весь исходный код вы можете просмотреть, перейдя по ссылке на страницу проекта в gitHub.
Цели
В этой части урока мы напишем игровой движок нашей «Змейки». И под созданием игрового движка подразумевается:
1. Написание классов, которые станут скелетом нашего приложения.
2. Предоставление им правильных методов и свойств, чтобы мы могли управлять ими по своему усмотрению.
3. Соединение всего в основном цикле приложения.
Классы
Теперь давайте разберем нашу игру на составные элементы: змея и игровое поле. Змея состоит из двух основных элементов: головы и хвоста. И надо не забыть, что змее нужно что-то есть.
Таким образом нам потребуется организовать следующую иерархию виджетов:
Игровое поле (Playground) Фрукты (Fruit) Змея (Snake) Голова (SnakeHead) Хвост (SnakeTail)
Мы объявим наши классы в файлах main.py и snake.kv , чтобы отделить дизайн от логики:
import kivy kivy.require('1.8.0') # Импортируем элементы Kivy, которые будем использовать в классах from kivy.app import App from kivy.uix.widget import Widget from kivy.properties import ObjectProperty class Playground(Widget): # Привязываем переменным элементы из .kv fruit = ObjectProperty(None) snake = ObjectProperty(None) class Fruit(Widget): pass class Snake(Widget): head = ObjectProperty(None) tail = ObjectProperty(None) class SnakeHead(Widget): pass class SnakeTail(Widget): pass class SnakeApp(App): def build(self): game = Playground() return game if __name__ == '__main__': SnakeApp().run()
#:kivy 1.8.0 snake: snake_id fruit: fruit_id Snake: id: snake_id Fruit: id: fruit_id head: snake_head_id tail: snake_tail_id SnakeHead: id: snake_head_id SnakeTail: id: snake_tail_id
Свойства
Теперь, когда мы реализовали классы, можно задуматься о содержимом.
Playground — это корневой виджет. Мы разделим его на сетку. Эта матрица поможет позиционировать и перемещать объекты по полю. Представление каждого дочернего виджета будет занимать одну клетку. Также нужно реализовать возможность сохранения счета и изменения частоты появления фруктов.
И последнее, но не менее важное: нужно реализовать управление вводом, но сделаем мы это в следующем разделе.
class Playground(Widget): fruit = ObjectProperty(None) snake = ObjectProperty(None) # Задаем размер сетки col_number = 16 row_number = 9 # Игровые переменные score = NumericProperty(0) turn_counter = NumericProperty(0) fruit_rythme = NumericProperty(0) # Одработка входных данных touch_start_pos = ListProperty() action_triggered = BooleanProperty(False)
Змея
Объект змеи не должен содержать ничего, кроме двух ее деталей: головы и хвоста.
Для головы мы должны знать текущее положение и направление движения для правильного графического представления: если змея движется направо — рисуем треугольник повернутый вправо, если змея движется влево — рисуем треугольник повернутый влево.
Позиция и направление будут соответствовать определенным инструкциям рисования. Для рисования треугольника нам нужно шесть точек (две координаты на одну вершину). Эти координаты будут не ячейками сетки, а определенными пикселями на холсте
Наконец, нам надо хранить информацию об объекте, нарисованном на холсте, чтобы удалить его позже (например, для перезапуска игры). Мы добавим логическую переменную, указывающую, должен ли объект быть нарисован на холсте:
class SnakeHead(Widget): # Направление головы и ее позиция direction = OptionProperty( "Right", options=["Up", "Down", "Left", "Right"]) x_position = NumericProperty(0) y_position = NumericProperty(0) position = ReferenceListProperty(x_position, y_position) # Представление на холсте points = ListProperty([0]*6) object_on_board = ObjectProperty(None) state = BooleanProperty(False)
Теперь хвост. Он состоит из блоков (изначально трех), занимающих одну ячейку. Когда «Змейка» будет двигаться, мы будем убирать самый последний блок хвоста и добавлять новый на предыдущую позицию головы:
class SnakeTail(Widget): # Длинна хвоста. Измеряется в количестве блоков size = NumericProperty(3) # Позицию каждого блока хвоста мы будем хранить здесь blocks_positions = ListProperty() # Обьекты (виджеты) хвоста будут находиться в этой переменной tail_blocks_objects = ListProperty()
Фрукт
Поведение фрукта похоже на поведение змеиной головы. Нам понадобится отслеживать его позицию, состояние и интервал появления:
class Fruit(Widget): # Эти значения будем использовать для определения частоты появления fruit_rhythme duration = NumericProperty(10) # Продолжительность существования interval = NumericProperty(3) # Продолжительность отсутствия # Представление на поле object_on_board = ObjectProperty(None) state = BooleanProperty(False)
В классе SnakeApp будет происходить запуск нашего приложения:
class SnakeApp(App): game_engine = ObjectProperty(None) def build(self): self.game_engine = Playground() return self.game_engine
Кое-что еще: нужно задать размеры виджетов. Каждый элемент будет занимать одну ячейку поля. Значит:
- высота виджета = высота поля / количество строк сетки;
- ширина виджета = ширина поля / количество колонок сетки.
Также нам нужно добавить виджет отображающий текущий счет.
Теперь snake.kv выглядит так:
#:kivy 1.8.0 snake: snake_id fruit: fruit_id Snake: id: snake_id width: root.width/root.col_number height: root.height/root.row_number Fruit: id: fruit_id width: root.width/root.col_number height: root.height/root.row_number Label: font_size: 70 center_x: root.x + root.width/root.col_number*2 top: root.top - root.height/root.row_number text: str(root.score) head: snake_head_id tail: snake_tail_id SnakeHead: id: snake_head_id width: root.width height: root.height SnakeTail: id: snake_tail_id width: root.width height: root.height
Методы
Начнем с класса змеи. Мы хотим устанавливать ее в начальное положение и получать информацию о том, где она находится и как взаимодействует с объектами поля:
class Snake(Widget): . def move(self): """ Движение змеи будет происходить в 3 этапа: - сохранить текущее положение головы. - передвинуть голову на одну позицию вперед. - переместить последний блок хвоста на предыдущие координаты головы . """ next_tail_pos = list(self.head.position) self.head.move() self.tail.add_block(next_tail_pos) def remove(self): """ Здесь мы опишем, удаление элементов хвоста и головы """ self.head.remove() self.tail.remove() def set_position(self, position): self.head.position = position def get_position(self): """ Положение змеи равно положению ее головы на поле. """ return self.head.position def get_full_position(self): """ Но иногда нам нужно будет узнавать, какое пространство занимает змея. """ return self.head.position + self.tail.blocks_positions def set_direction(self, direction): self.head.direction = direction def get_direction(self): return self.head.direction
Мы назвали ряд методов. Теперь давайте их реализуем. Начнем с remove() и add_block() :
class SnakeTail(Widget): . def remove(self): # Сбрасываем счетчик длины self.size = 3 # Удаляем каждый блок хвоста for block in self.tail_blocks_objects: self.canvas.remove(block) # Обнуляем списки с координатами блоков # и их представления на холсте self.blocks_positions = [] self.tail_blocks_objects = [] def add_block(self, pos): """ Здесь действуем в 3 этапа : - Передаем позицию нового блока как аргумент и добавляем блок в список объектов. - Проверяем равенство длины хвоста и количества блоков и изменяем, если требуется. - Рисуем блоки на холсте, до тех пор, пока количество нарисованных блоков не станет равно длине хвоста. """ # Добавляем координаты блоков в список self.blocks_positions.append(pos) # Делаем проверку соответствия количеству блоков змеи на холсте и переменной отражающей длину if len(self.blocks_positions) > self.size: self.blocks_positions.pop(0) with self.canvas: # Рисуем блоки используя координаты из списка for block_pos in self.blocks_positions: x = (block_pos[0] - 1) * self.width y = (block_pos[1] - 1) * self.height coord = (x, y) block = Rectangle(pos=coord, size=(self.width, self.height)) # Добавляем новый блок к списку объектов self.tail_blocks_objects.append(block) # Делаем проверку длины и удаляем лишние блоки с холста, если необходимо if len(self.tail_blocks_objects) > self.size: last_block = self.tail_blocks_objects.pop(0) self.canvas.remove(last_block)
Теперь работаем с головой. Она будет иметь две функции: move() и remove():
class SnakeHead(Widget): # Представление головы на "сетке" direction = OptionProperty( "Right", options=["Up", "Down", "Left", "Right"]) x_position = NumericProperty(0) y_position = NumericProperty(0) position = ReferenceListProperty(x_position, y_position) # Представление головы на поле points = ListProperty([0] * 6) object_on_board = ObjectProperty(None) state = BooleanProperty(False) def is_on_board(self): return self.state def remove(self): if self.is_on_board(): self.canvas.remove(self.object_on_board) self.object_on_board = ObjectProperty(None) self.state = False def show(self): """ Размещаем голову на холсте. """ with self.canvas: if not self.is_on_board(): self.object_on_board = Triangle(points=self.points) self.state = True else: # Если объект должен быть на поле - удаляем старую голову # и рисуем новую self.canvas.remove(self.object_on_board) self.object_on_board = Triangle(points=self.points) def move(self): """ Не самое элегантное решение, но это работает. Здесь мы описываем изображение треугольника для каждого положения головы. """ if self.direction == "Right": # Обновляем позицию self.position[0] += 1 # Вычисляем положения точек x0 = self.position[0] * self.width y0 = (self.position[1] - 0.5) * self.height x1 = x0 - self.width y1 = y0 + self.height / 2 x2 = x0 - self.width y2 = y0 - self.height / 2 elif self.direction == "Left": self.position[0] -= 1 x0 = (self.position[0] - 1) * self.width y0 = (self.position[1] - 0.5) * self.height x1 = x0 + self.width y1 = y0 - self.height / 2 x2 = x0 + self.width y2 = y0 + self.height / 2 elif self.direction == "Up": self.position[1] += 1 x0 = (self.position[0] - 0.5) * self.width y0 = self.position[1] * self.height x1 = x0 - self.width / 2 y1 = y0 - self.height x2 = x0 + self.width / 2 y2 = y0 - self.height elif self.direction == "Down": self.position[1] -= 1 x0 = (self.position[0] - 0.5) * self.width y0 = (self.position[1] - 1) * self.height x1 = x0 + self.width / 2 y1 = y0 + self.height x2 = x0 - self.width / 2 y2 = y0 + self.height # Записываем положения точек self.points = [x0, y0, x1, y1, x2, y2] # Рисуем голову self.show()
А что там с фруктами? Мы должны уметь помещать их в заданные координаты и удалять, когда нам это понадобится:
class Fruit(Widget): . def is_on_board(self): return self.state def remove(self, *args): # Удаляем объект с поля и указываем, что он сейчас стерт if self.is_on_board(): self.canvas.remove(self.object_on_board) self.object_on_board = ObjectProperty(None) self.state = False def pop(self, pos): self.pos = pos # объявляем, что фрукт находится на поле # Рисуем фрукт with self.canvas: x = (pos[0] - 1) * self.size[0] y = (pos[1] - 1) * self.size[1] coord = (x, y) # Сохраняем представление и обновляем состояние объекта self.object_on_board = Ellipse(pos=coord, size=self.size) self.state = True
Почти готово, не сдавайтесь! Теперь нужно организовать весь игровой процесс, который будет происходить в классе Playground. Рассмотрим логику игры: она начинается с того, что змея помещается в случайные координаты. Игра обновляется при каждом перемещении змеи. Во время обновлений мы проверяем направление змеи и ее положение. Если змея сталкивается сама с собой или выходит за пределы поля – мы засчитываем поражение и игра начинается сначала.
Как будет осуществляться управление? Когда игрок касается экрана, мы сохраняем координаты касания. Когда палец будет перемещаться, мы будем сравнивать новое положение с исходным. Если позиция будет изменена на 10 % от размера экрана, мы будем определять это как инструкцию и обрабатывать ее:
class Playground(Widget): . def start(self): # Добавляем змею на поле self.new_snake() # Начинаем основной цикл обновления игры self.update() def reset(self): # Сбрасываем игровые переменные self.turn_counter = 0 self.score = 0 # Удаляем образы змеи и фрукта с поля self.snake.remove() self.fruit.remove() def new_snake(self): # Генерируем случайные координаты start_coord = ( randint(2, self.col_number - 2), randint(2, self.row_number - 2)) # Устанавливаем для змеи новые координаты self.snake.set_position(start_coord) # Генерируем случайное направление rand_index = randint(0, 3) start_direction = ["Up", "Down", "Left", "Right"][rand_index] # Задаем змее случайное направление self.snake.set_direction(start_direction) def pop_fruit(self, *args): # Генерируем случайные координаты для фрукта random_coord = [ randint(1, self.col_number), randint(1, self.row_number)] # получаем координаты всех клеток занимаемых змеей snake_space = self.snake.get_full_position() # Если координаты фрукта совпадают с координатами клеток змеи - генерируем # новые координаты while random_coord in snake_space: random_coord = [ randint(1, self.col_number), randint(1, self.row_number)] # Помещаем образ фрукта на поле self.fruit.pop(random_coord) def is_defeated(self): """ Проверяем, является ли позиция змеи проигрышной. """ snake_position = self.snake.get_position() # Если змея кусает свой хвост - поражение if snake_position in self.snake.tail.blocks_positions: return True # Если вышла за пределы поля - поражение if snake_position[0] > self.col_number \ or snake_position[0] < 1 \ or snake_position[1] >self.row_number \ or snake_position[1] < 1: return True return False def update(self, *args): """ Используется для смены игровых ходов. """ # Перемещаем змею на следующую позицию self.snake.move() # Проверяем на поражение # Если поражение - сбрасываем игру if self.is_defeated(): self.reset() self.start() return # Проверяем, находится ли фрукт на поле if self.fruit.is_on_board(): # Если змея съела фрукт - увеличиваем счет и длину змеи if self.snake.get_position() == self.fruit.pos: self.fruit.remove() self.score += 1 self.snake.tail.size += 1 # Увеличиваем счетчик ходов self.turn_counter += 1 def on_touch_down(self, touch): self.touch_start_pos = touch.spos def on_touch_move(self, touch): # Вычисляем изменение позиции пальца delta = Vector(*touch.spos) - Vector(*self.touch_start_pos) # Проверяем, изменение >10% от размера экрана: if not self.action_triggered \ and (abs(delta[0]) > 0.1 or abs(delta[1]) > 0.1): # Если да, задаем змее подходящее направление if abs(delta[0]) > abs(delta[1]): if delta[0] > 0: self.snake.set_direction("Right") else: self.snake.set_direction("Left") else: if delta[1] > 0: self.snake.set_direction("Up") else: self.snake.set_direction("Down") # Здесь мы регистрируем, что действие закончено, для того, чтобы оно не # происходило более одного раза за ход self.action_triggered = True def on_touch_up(self, touch): # Указываем, что мы готовы принять новые инструкции self.action_triggered = False
Основной цикл
Здесь происходят процессы, устанавливающие положение фрукта, управляющие движением змеи и определяющие проигрыш:
def update(self, *args): """ Используется для смены игровых ходов. """ # Регистрация последовательности появления фруктов в планировщике событий if self.turn_counter == 0: self.fruit_rythme = self.fruit.interval + self.fruit.duration Clock.schedule_interval( self.fruit.remove, self.fruit_rythme) elif self.turn_counter == self.fruit.interval: self.pop_fruit() Clock.schedule_interval( self.pop_fruit, self.fruit_rythme) . # Каждое обновление будет происходить ежесекундно (1'') Clock.schedule_once(self.update, 1)
Нужно добавить обработчик для события сброса игры:
def reset(self): . Clock.unschedule(self.pop_fruit) Clock.unschedule(self.fruit.remove) Clock.unschedule(self.update)
Теперь мы можем протестировать игру.
Одна важная деталь. Чтобы приложение запустилось с правильным разрешением экрана, нужно сделать так:
class SnakeApp(App): game_engine = ObjectProperty(None) def on_start(self): self.game_engine.start() .
И вуаля! Теперь вы можете запустить приложение. Остается только упаковать его с помощью buildozer и загрузить на устройство.
Создаем экраны
В приложении будет два экрана: приветствия и игровой. Также будет всплывающее меню настроек. Сначала мы сделаем макеты наших виджетов в .kv-файле, а потом напишем соответствующие классы Python.
Внешняя оболочка
PlaygroundScreen содержит только игровое поле:
: game_engine: playground_widget_id Playground: id: playground_widget_id
Основной экран будет состоять из трех встроенных виджетов: название приложения, кнопки запуска игры и кнопки вызывающая меню настроек:
AnchorLayout: anchor_x: "center" BoxLayout: orientation: "vertical" size_hint: (0.5, 1) spacing: 10 Label: size_hint_y: .4 text: "Ouroboros" valign: "bottom" bold: True font_size: 50 padding: 0, 0 AnchorLayout: anchor_x: "center" size_hint_y: .6 BoxLayout: size_hint: .5, .5 orientation: "vertical" spacing: 10 Button: halign: "center" valign: "middle" text: "Play" Button: halign: "center" valign: "middle" text: "Options"
Всплывающее окно будет занимать ¾ экрана приветствия. Оно будет содержать виджеты, необходимые для установки параметров, и кнопку «Сохранить».
title: "Options" size_hint: .75, .75 BoxLayout: orientation: "vertical" spacing: 20 GridLayout: size_hint_y: .8 cols: 2 AnchorLayout: anchor_x: "center" size_hint: 1, .25 Button: size_hint_x: 0.5 text: "Save changes" on_press: root.dismiss()
Классы
На экране приветствия требуется только метод show_popup() , который будет вызываться при нажатии кнопки настроек на главном экране. Нам не нужно определять что-либо еще для кнопки включения игры, потому что она будет использовать способность своего родителя обращаться к диспетчеру экрана:
class WelcomeScreen(Screen): options_popup = ObjectProperty(None) def show_popup(self): # Создаем экземпляр всплывающего окна и отображаем на экране self.options_popup = OptionsPopup() self.options_popup.open()
Теперь нужно сделать так, чтоб экран приветствия показывался при запуске игры, а игра начиналась только тогда, когда будет показано игровое поле:
class PlaygroundScreen(Screen): game_engine = ObjectProperty(None) def on_enter(self): # Показываем экран и начинаем игру self.game_engine.start()
Также нам нужно подготовить класс окна настроек, иначе привязка будет работать не правильно:
class OptionsPopup(Popup): pass
Теперь добавим ScreenManager в приложение и зарегистрируем два экрана:
class SnakeApp(App): screen_manager = ObjectProperty(None) def build(self): # Объявление SkreenManager как свойства класса SnakeApp.screen_manager = ScreenManager() # Создание экземплров экранов ws = WelcomeScreen(name="welcome_screen") ps = PlaygroundScreen(name="playground_screen") # Регистрация экранов в SkreenManager self.screen_manager.add_widget(ws) self.screen_manager.add_widget(ps) return self.screen_manager
Теперь нужно сказать кнопкам, что делать, когда на них нажимают:
. Button: . on_press: root.manager.current = "playground_screen" Button: . on_press: root.show_popup() . Button: . on_press: root.dismiss()
После проигрыша нужно возвращаться обратно на экран приветствия:
class Playground(Widget): . def update(self, *args): . # Проверяем проигрыш # Если это произошло, # показываем экран приветствия if self.is_defeated(): self.reset() SnakeApp.screen_manager.current = "welcome_screen" return …
Добавляем настройки
У нас будет всего два параметра:
- Включение/отключение границ. Если границы включены, при выхождении змеи за пределы экрана засчитывается проигрыш. Если границы выключены, змея будет появляться на другой стороне, если выходит за пределы.
- Скорость змеи.
Добавляем необходимые виджеты во всплывающее окно:
border_option_widget: border_option_widget_id speed_option_widget: speed_option_widget_id title: "Options" size_hint: .75, .75 BoxLayout: orientation: "vertical" spacing: 20 GridLayout: size_hint_y: .8 cols: 2 Label: text: "Borders" halign: "center" Switch: id: border_option_widget_id Label: text: "Game speed" halign: "center" Slider: id: speed_option_widget_id max: 10 min: 1 step: 1 value: 1
Теперь подготовим классы, чтобы они могли изменяться вместе с настройками. Если границы включены, будем рисовать очертание игрового поля. Также добавим возможность изменения частоты обновления:
class Playground(Widget): . # Пользовательские настройки start_speed = NumericProperty(1) border_option = BooleanProperty(False) . #Игровые переменные . start_time_coeff = NumericProperty(1) running_time_coeff = NumericProperty(1) . def start(self): # Если границы включены, рисуем прямоугольник вокруг поля if self.border_option: with self.canvas.before: Line(width=3., rectangle=(self.x, self.y, self.width, self.height)) # Вычисляем коэффициент изменения частоты обновления игры # (по умолчанию 1.1, максимальный 2) self.start_time_coeff += (self.start_speed / 10) self.running_time_coeff = self.start_time_coeff . def reset(self): # Сбрасываем игровые переменные . self.running_time_coeff = self.start_time_coeff . def is_defeated(self): . # Если змея вышла за границы, которые были включены -- поражение if self.border_option: if snake_position[0] > self.col_number \ or snake_position[0] < 1 \ or snake_position[1] >self.row_number \ or snake_position[1] < 1: return True return False def handle_outbound(self): """ Используется для перемещения змеи на противоположную сторону (только если границы выключены) """ position = self.snake.get_position() direction = self.snake.get_direction() if position[0] == 1 and direction == "Left": self.snake.tail.add_block(list(position)) self.snake.set_position([self.col_number + 1, position[1]]) elif position[0] == self.col_number and direction == "Right": self.snake.tail.add_block(list(position)) self.snake.set_position([0, position[1]]) elif position[1] == 1 and direction == "Down": self.snake.tail.add_block(list(position)) self.snake.set_position([position[0], self.row_number + 1]) elif position[1] == self.row_number and direction == "Up": self.snake.tail.add_block(list(position)) self.snake.set_position([position[0], 0]) def update(self, *args): . # Изменяем частоту появления фрукта if self.turn_counter == 0: . Clock.schedule_interval( self.fruit.remove, self.fruit_rythme / self.running_time_coeff) elif self.turn_counter == self.fruit.interval: . Clock.schedule_interval( self.pop_fruit, self.fruit_rythme / self.running_time_coeff) # Проверяем, пересечение змеей границ # если пересекает -- переносим на противоположную сторону if not self.border_option: self.handle_outbound() . # Проверяем готовность фрукта if self.fruit.is_on_board(): if self.snake.get_position() == self.fruit.pos: . self.running_time_coeff *= 1.05 . Clock.schedule_once(self.update, 1 / self.running_time_coeff)
Изменим всплывающее окно так, чтобы оно могло передавать значения:
class OptionsPopup(Popup): border_option_widget = ObjectProperty(None) speed_option_widget = ObjectProperty(None) def on_dismiss(self): Playground.start_speed = self.speed_option_widget.value Playground.border_option = self.border_option_widget.active
Готово. Теперь можно упаковать проект и играть:
buildozer android debug # Создает apk-файл в папке ./bin buildozer android debug deploy # Установка на Android-устройство
Программирование: пишем игры на Python, часть 1

![]()
О чём материал? Создание игр, программирование с нуля: для детей и подростков, а также их родителей и учителей! Материал для всех, кто волею судеб вынужден заняться программированием. Или просто по жизни творческий, увлекающийся человек, которому интересно изучать что-то новое и реализовывать себя в современности. И не забываем: изучение языков программирования и алгоритмов повышает логику и интеллект. Пишем игры: изучаем программирование на Python на примере написания простых, но демонстрирующих возможности программирования игр. Почему именно этот учебник? Я уже почти два десятка лет работаю преподавателем информатики и сталкиваюсь с одной иногда раздражающей вещью. Большинство материалов, призванных "научить программированию", на самом деле не учат, а являются своеобразными справочниками по языку: синтаксис, функции, результат. Согласитесь, даже если мы выучим весь русско-английский словарь, то на английском не заговорим. Потому что для разговора нужно знать ещё тысячу тонкостей: времена, склонения, использование местоимений и предлогов и прочее. В этом учебнике я рассказываю не только о языке Python, но и веду читателя путём рассуждений, логический умозаключений, отвечая не только вопрос "С помощью чего?", но и "Для чего?" и "Почему?" Вся теория сразу же найдёт отражение в практике. СТРУКТУРА МАТЕРИАЛА: - базовые сведения о языке Python: где скачать, как установить, каким образом пользоваться, что делать для получения результата, реализация простейших алгоритмов, некоторые хитрости и тонкости, словом, дружеские советы; - архитектура игр: на каких принципах строятся игры, что нужно предусмотреть, как выстроить систему получения и обработки данных; - игры: в этой части представлено четыре игры: 1. «Угадай число». Цель игры: развлечение и анализ числовых рядов. Подойдёт для детей, которые учатся считать. Вы, если вы родитель, можете написать игру конкретно для своего ребёнка, заложив в программу особенности и пожелания. Например, выбрать диапазон чисел или установить правила счёта. 2. «Учись считать». Цель игры: развитие навыков счёта, сложение, вычитание, умножение и деление. Работа со временем (на скорость). Подойдёт вообще всем - и даже взрослым, которые желают повысить навык и увеличить скорость правильного счёта "в уме". 3. «Казино 678». Цель игры: антипропаганда азартных игр. Когда вы увидите алгоритм, на котором пусть даже приблизительно строится большинство азартных игр, различных «одноруких бандитов», казино и прочего - отобьёт надежду на получение прибыли напрочь. Когда вы своими руками напишите алгоритм и, проигрывая виртуальные деньги (которых сможете поставить себе сколько угодно!), поймёте, что выиграть попросту нельзя, сразу интерес к азартным играм пропадёт. Рекомендовано для подростков с надеждой внезапно обогатиться на как минимум «миллион долларов» и жить припеваючи. 4. «Ипподром». Цель игры: изучение влияния скрытых параметров на обработку данных, практическое умение разработать и реализовать системную взаимосвязь между изменением видимых данных и случайными значениями. Побочный эффект: антипропаганда азартных игр и просто интересно 🙂 Все представленные алгоритмы направлены на воспитание: - понимания внутренних принципов работы процессора; - практического умения создавать и записывать алгоритмы на конкретном языке программирования; - практического умения реализовывать обработку данных с помощью инструментов Python; - . и популяризацию творческого, интересного и созидательного времяпровождения. Вы найдёте: - систематизированную информацию по базовым конструкциям языка; - базовые алгоритмы обработки данных; - практические советы и замечания, выработанные на основе многолетнего опыта; - примеры рабочего кода; - взгляд на архитектуру современных игр; - основные этапы проектирования алгоритмов для игр. Пожалуйста, если вам понравилось приложение, - поставьте оценку и напишите комментарий. Это очень мотивирует на продолжение работы 🙂
Что нового
- критическое обновление системы подтверждения платежа. Обязательно обновите приложение, иначе что-то может работать некорректно и непредсказуемо, как вся наша жизнь 🙂
Разработка игры под Android на Python на базе Kivy. От А до Я: подводные камни и неочевидные решения. Часть 1
Некоторое время тому назад я решил попробовать написать что-то на Python под Android. Такой странный для многих выбор обусловлен тем, что я люблю Python и люблю Android, а ещё люблю делать необычное (ну хорошо, не самое обычное). В качестве фреймворка был выбран Kivy — фактически, безальтернативный вариант, но он мне очень понравился. Однако, по нему не так уж много информации (нет, документация отличная, но иногда её недостаточно), особенно на русском языке, а некоторые вещи хоть и можно реализовать, но их то ли никто раньше не делал, то ли не счёл нужным поделиться информацией. Ну а я счёл 🙂 И этот пост тому результатом.
Под катом я расскажу обо всех этапах разработки, о том, как развивалась простая идея и как для этого приходилось искать новые возможности, о возникших подводных камнях и багах, о неочевидных решениях и устаревшей документации 🙂 Цель — описать в одном тексте основные пункты, чтобы человеку, решившему написать что-то немного сложнее игры Pong из официального туториала, не приходилось перерывать официальный форум поддержки и StackOverflow и тратить часы на то, что делается за пару минут, если знаешь, как.
0. Если вы впервые слышите о Kivy.
… то всё зависит от того, любите ли вы Python и Android, и интересно ли вам в этом разобраться. Если нет — проще забить 🙂 А если да, то начать нужно с официальной документации, гайдов, и уже упомянутого официального туториала по игре Pong — это даст базовое представление о фреймворке и его возможностях. Я же не буду останавливаться на столь тривиальных вещах (тем более, для понимания базовых принципов туториал отлично подходит) и сразу пойду дальше. Будем считать, что это было вступление 🙂
1. Немного о моей игре
Для начала нужна была идея. Мне хотелось что-то достаточно простое, чтобы оценить возможности фреймворка, но и достаточно интересное и оригинальное, чтобы не программировать ради программирования (это здорово, но когда это не единственная цель — это ещё лучше). Я неплохо проектирую интерфейсы, но не умею рисовать, поэтому игра должна была быть простая графически, или вообще текстовая. И тут так уж сложилось, что у меня есть заброшенный сайт с цитатами, с которого я когда-то начинал свой путь в web-разработке (я о нём даже писал на Хабре много лет назад). Поэтому идея возникла такая: игра-викторина «Угадай цитату». В русскоязычном Google Play ничего подобного не было, а в англоязычном была пара поделок низкого качества с сотней скачиваний.
Почти сразу же стало понятно, что просто так отгадывать цитату за цитатой — скучно. Так появились первые «фишки», которые, в итоге, и определили итоговую игру. В первую очередь это были тематические пакеты (то есть пакеты цитат, объединённые одной темой или автором) и баллы (которые начисляются за отгадывание цитат и прохождение пакетов, и тратятся на подсказки и разблокировку новых тем), а также статистика, достижения и избранное.
Немного картинок
Так всё начиналось (кликабельно):

Ну ладно, ладно, больше не буду показывать такой ужас 🙂 Кстати, вот так оно выглядит сейчас (тоже кликабельно, скрины взяты с Google Play):

Первые проблемы начались с первого же экрана…
2. Kivy тормоз или я что-то делаю не так?
Один мой друг любит отвечать на такие вопросы «да» 🙂 На самом деле, некоторые вещи в Kivy действительно работают медленно, например, создание виджетов. Но это не значит, что это дело нельзя оптимизировать. Об этом я и расскажу.
Так как цитаты и темы хранятся в БД, то, само собой, кнопки с пакетами генерируются динамически. И вот тут-то я обнаружил, что происходит это очень медленно: примерно полсекунды на список из 20 кнопок. Возможно, это и не очень много при загрузке приложения, но при переходе на главный экран из других внутренних экранов приложения — непозволительно много. Здесь стоит отметить, что кнопка к тому моменту уже представляла собой, на самом деле, набор из нескольких элементов, визуально составляющих одну кнопку:

Первым моим побуждением было тем или иным образом закешировать их, и, действительно, опыт показал, что если создать все виджеты заранее, и сохранить их как свойство объекта StartScreen, то всё (кроме первой генерации) работает достаточно быстро. Однако же, данные в кнопках нужно периодически обновлять (хотя бы то же количество отгаданных цитат). Да и загрузку новых пакетов я уже тогда планировал. Конечно, не проблема реализовать и это, но я решил не изобретать велосипед и подумать.
Сначала стоило убедиться, что проблема именно в создании виджетов, поэтому я за несколько минут набросал простенькое приложение на два экрана, в каждом из которых генерировался набор строк из лейбла и чекбокса количеством 50 шт. 🙂
Исходный код тестового приложения
from kivy.app import App from kivy.uix.screenmanager import ScreenManager, Screen from kivy.uix.boxlayout import BoxLayout from kivy.properties import ObjectProperty, StringProperty from kivy.clock import Clock from time import time class ListScreen(Screen): items_box = ObjectProperty(None) def on_enter(self): start = time() for i in range(0,50): self.items_box.add_widget(ListItem('Item '+str(i))) self.items_box.bind(minimum_height=self.items_box.setter('height')) print time()-start def on_leave(self): self.items_box.clear_widgets() class ListItem(BoxLayout): title = StringProperty('') def __init__(self, title, **kwargs): super(ListItem, self).__init__(**kwargs) self.title = title class ListApp(App): sm = ScreenManager() screens = <> def build(self): self.__create_screens() ListApp.sm.add_widget(ListApp.screens['list1']) Clock.schedule_interval(self._switch, 1) return ListApp.sm def _switch(self, *args): ListApp.sm.switch_to(ListApp.screens['list1' if ListApp.sm.current != 'list1' else 'list2']) def __create_screens(self): ListApp.screens['list1'] = ListScreen(name='list1') ListApp.screens['list2'] = ListScreen(name='list2') if __name__ == '__main__': ListApp().run()
: items_box: items_box BoxLayout: orientation: "vertical" AnchorLayout: size_hint_y: 0.1 padding: self.width*0.1, self.height*0.05 Label: font_size: root.height*0.05 text: "Some list" ScrollView: size_hint_y: 0.9 size: self.size BoxLayout: id: items_box orientation: "vertical" padding: self.width*0.1, 0 size_hint_y: None : orientation: "horizontal" size_hint_y: None height: app.sm.height*0.1 Label: font_size: app.sm.height*0.025 text: root.title size_hint_x: 0.9 text_size: self.size valign: "middle" CheckBox size_hint_x: 0.1
Запустил на своём стареньком Moto G (gen3) и получил:
11-28 11:44:09.525 1848 2044 I python : 0.5793800354 11-28 11:44:10.853 1848 2044 I python : 0.453143119812 11-28 11:44:12.544 1848 2044 I python : 0.633069992065 11-28 11:44:13.697 1848 2044 I python : 0.369570970535 11-28 11:44:14.988 1848 2044 I python : 0.594089031219
И далее в том же духе. Поиск по этому вопросу ничего не дал, поэтому я обратился к разработчикам. И получил ответ: «Создание виджетов относительно медленное, особенно в зависимости от того, что они содержат. Для создания больших списков лучше использовать RecycleView». Здесь хочу пояснить, почему я вообще описываю этот момент, ведь описание RecycleView есть в документации. Да, действительно, есть, но мало кто способен изучить и запомнить всю документацию перед тем, как начнёт разработку, и найти нужный инструмент бывает непросто, особенно если он нигде не описан в контексте решения конкретной проблемы. Теперь же он описан 🙂
Исходный код тестового приложения с RecycleView
from kivy.app import App from kivy.uix.screenmanager import ScreenManager, Screen from kivy.properties import ObjectProperty from kivy.clock import Clock from time import time class ListScreen(Screen): recycle_view = ObjectProperty(None) items_box = ObjectProperty(None) def on_enter(self): start = time() for i in range(0,50): self.recycle_view.data.append() print time()-start def on_leave(self): self.recycle_view.data = [] class ListApp(App): sm = ScreenManager() screens = <> def build(self): self.__create_screens() ListApp.sm.add_widget(ListApp.screens['list1']) Clock.schedule_interval(self._switch, 1) return ListApp.sm def _switch(self, *args): ListApp.sm.switch_to(ListApp.screens['list1' if ListApp.sm.current != 'list1' else 'list2']) def __create_screens(self): ListApp.screens['list1'] = ListScreen(name='list1') ListApp.screens['list2'] = ListScreen(name='list2') if __name__ == '__main__': ListApp().run()
: recycle_view: recycle_view items_box: items_box BoxLayout: orientation: "vertical" AnchorLayout: size_hint_y: 0.1 padding: self.width*0.1, self.height*0.05 Label: font_size: root.height*0.05 text: "Some list" RecycleView: id: recycle_view size_hint: 1, 0.9 viewclass: "ListItem" RecycleBoxLayout: id: items_box orientation: "vertical" padding: self.width*0.1, 0 default_size_hint: 1, None size_hint: 1, None height: self.minimum_height : orientation: "horizontal" size_hint: 1, None height: app.sm.height*0.1 title: '' Label: font_size: app.sm.height*0.025 text: root.title size_hint_x: 0.9 text_size: self.size valign: "middle" CheckBox size_hint_x: 0.1
11-29 13:11:58.196 13121 13203 I python : 0.00388479232788 11-29 13:11:59.192 13121 13203 I python : 0.00648307800293 11-29 13:12:00.189 13121 13203 I python : 0.00288391113281 11-29 13:12:01.189 13121 13203 I python : 0.00324606895447 11-29 13:12:03.188 13121 13203 I python : 0.00265002250671
Более чем в 100 раз быстрее. Впечатляет, не правда ли?
В завершение следует упомянуть, что RecycleView — не панацея. Он не подходит, если размер элемента зависит от содержимого (например, Label, размер которого меняется в зависимости от количества текста).
3. Сервисы. Автозапуск и перезапуск
Следующая проблема, с которой я столкнулся, не поддавалась решению так долго, что я уже малодушно подумывал счесть данный фреймворк непригодным и забить 🙂 Проблема была с сервисами (в Android так называется процессы, выполняющиеся в фоновом режиме). Создать сервис не так уж и сложно — немного сбивает с толку устаревшая документация, но и только. Однако, в большинстве случаев, много ли толку от сервиса, который, во-первых, не запускается автоматически при загрузке телефона, а во-вторых, не перезапускается, если «выбросить» приложение свайпом из диспетчера задач? По-моему, нет.
На тот момент по этой теме была всего лишь одна статья в официальной wiki, но она, хоть и называлась «Starting Kivy service on bootup», на самом деле всего лишь рассказывала, как при загрузке телефона запустить приложение, но не его сервис (да, такое тоже бывает полезно, но значительно реже, как по мне). Ту статью я, в итоге, переписал, а здесь расскажу подробности.
Допустим, у нас есть примитивный сервис, который всего-то и делает, что периодически выводит в лог строку (этим мы заранее исключаем баги, которые могут возникать из-за особенностей самого сервиса).
from time import sleep if __name__ == '__main__': while True: print "myapp service" sleep(5)
Из приложения мы запускаем его методом основного класса при помощи PyJnius:
from jnius import autoclass # . # class GuessTheQuoteApp(App): # . # def __start_service(self): service = autoclass('com.madhattersoft.guessthequote.ServiceGuessthequoteservice') mActivity = autoclass('org.kivy.android.PythonActivity').mActivity service.start(mActivity, "")
Если APK собран правильно, при запуске приложения сервис будет стартовать, но этого недостаточно.
Для начала, попробуем сделать так, чтобы он перезапускался при остановке приложения (например, при снятии его из диспетчера задач). Конечно, можно было бы использовать startForeground, но это уже не совсем фоновое выполнение задачи 🙂 Для него потребуется, как минимум, уведомление — это не всегда подходит. В данном случае идеально подходит флаг START_STICKY, но мы же пишем на Python, что делает задачу не столь тривиальной — по крайней мере, при помощи PyJnius она уже не решается.
Честно говоря, она вообще решается достаточно криво, поскольку я пока не готов становиться одним из разработчиков Python4Android, благодаря которому всё это счастье вообще работает. А изменения нужно вносить именно в код Python4Android. А конкретно, нам нужен файл .buildozer/android/platform/build/dists/guessthequote/src/org/kivy/android/PythonService.java в котором в функции startType() мы меняем флаг START_NOT_STICKY на START_STICKY:
public int startType()
Ура, сервис рестартится. Всё? Конечно, нет 🙂 Потому что он тут же валится с ошибкой:
E AndroidRuntime: Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'android.os.Bundle android.content.Intent.getExtras()' on a null object reference
Проблема в функции onStartCommand(Intent intent, int flags, int startId), поскольку после перезапуска intent у нас null. Что ж, перепишем и её:
@Override public int onStartCommand(Intent intent, int flags, int startId) < if (pythonThread != null) < Log.v("python service", "service exists, do not start again"); return START_NOT_STICKY; >if (intent != null) < startIntent = intent; Bundle extras = intent.getExtras(); androidPrivate = extras.getString("androidPrivate"); androidArgument = extras.getString("androidArgument"); serviceEntrypoint = extras.getString("serviceEntrypoint"); pythonName = extras.getString("pythonName"); pythonHome = extras.getString("pythonHome"); pythonPath = extras.getString("pythonPath"); pythonServiceArgument = extras.getString("pythonServiceArgument"); pythonThread = new Thread(this); pythonThread.start(); if (canDisplayNotification()) < doStartForeground(extras); >> else < pythonThread = new Thread(this); pythonThread.start(); >return startType(); >
F DEBUG : Abort message: 'art/runtime/java_vm_ext.cc:410] JNI DETECTED ERROR IN APPLICATION: GetStringUTFChars received NULL jstring'
Проблема в том, что функция nativeStart() не получает нужных Extras. К сожалению, два из них мне пришлось захардкодить. В итоге выглядит это так:
@Override public void run() < String package_root = getFilesDir().getAbsolutePath(); String app_root = package_root + "/app"; File app_root_file = new File(app_root); PythonUtil.loadLibraries(app_root_file); this.mService = this; if (androidPrivate == null) < androidPrivate = package_root; >if (androidArgument == null) < androidArgument = app_root; >if (serviceEntrypoint == null) < serviceEntrypoint = "./service/main.py"; // hardcoded >if (pythonName == null) < pythonName = "guessthequoteservice"; // hardcoded >if (pythonHome == null) < pythonHome = app_root; >if (pythonPath == null) < pythonPath = package_root; >if (pythonServiceArgument == null) < pythonServiceArgument = app_root+":"+app_root+"/lib"; >nativeStart( androidPrivate, androidArgument, serviceEntrypoint, pythonName, pythonHome, pythonPath, pythonServiceArgument); stopSelf(); >
Перейдём к автозапуску сервиса при запуске телефона. После предыдущей проблемы это будет уже проще. (На самом деле же всё было наоборот — я очень долго не мог понять, что именно нужно добавить, поскольку информации об этом не было вообще нигде, и сами разработчики тоже не знали, как решить данную задачу. И только разобравшись параллельно с вопросом перезапуска, я понял, что нужно сделать.)
Для начала понадобится разрешение RECEIVE_BOOT_COMPLETED — это просто. А затем — BroadcastReceiver, его придётся добавить в AndroidManifest вручную, но это тоже не проблема. Проблема в том, что в нём писать 🙂
Решение для запуска приложения (не сервиса) выглядит так:
package com.madhattersoft.guessthequote; import android.content.BroadcastReceiver; import android.content.Intent; import android.content.Context; import org.kivy.android.PythonActivity; public class MyBroadcastReceiver extends BroadcastReceiver < public void onReceive(Context context, Intent intent) < Intent ix = new Intent(context, PythonActivity.class); ix.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(ix); >>
Сначала я попытался просто переписать его для сервиса:
package com.madhattersoft.guessthequote; import android.content.BroadcastReceiver; import android.content.Intent; import android.content.Context; import com.madhattersoft.guessthequote.ServiceGuessthequoteservice; public class MyBroadcastReceiver extends BroadcastReceiver < public void onReceive(Context context, Intent intent) < Intent ix = new Intent(context, ServiceGuessthequoteservice.class); ix.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startService(ix); >>
E AndroidRuntime: java.lang.RuntimeException: Unable to start service com.madhattersoft.guessthequote.ServiceGuessthequoteservice@8c96929 with Intent < cmp=com.madhattersoft.guessthequote/.ServiceGuessthequoteservice >: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.os.Bundle.getString(java.lang.String)' on a null object reference
Думаю, вам уже понятно, что проблема в тех самых Extras. Мне же тогда об этом было узнать неоткуда. Но не буду тянуть, рабочий код выглядит так:
package import com.madhattersoft.guessthequote; import android.content.BroadcastReceiver; import android.content.Intent; import android.content.Context; import com.madhattersoft.guessthequote.ServiceGuessthequoteservice; public class MyBroadcastReceiver extends BroadcastReceiver < public void onReceive(Context context, Intent intent) < String package_root = context.getFilesDir().getAbsolutePath(); String app_root = package_root + "/app"; Intent ix = new Intent(context, ServiceGuessthequoteservice.class); ix.putExtra("androidPrivate", package_root); ix.putExtra("androidArgument", app_root); ix.putExtra("serviceEntrypoint", "./service/main.py"); ix.putExtra("pythonName", "guessthequoteservice"); ix.putExtra("pythonHome", app_root); ix.putExtra("pythonPath", package_root); ix.putExtra("pythonServiceArgument", app_root+":"+app_root+"/lib"); ix.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startService(ix); >>
Локализация и мультиязычность
В целом, для локализации можно использовать gettext, или же поступить ещё проще — создать папку lang, в ней по файлу на каждый язык (например, en.py и ru.py), определить там все слова и фразы в виде переменных/констант, и далее подключить нужный модуль. Примерно так:
if autoclass('java.util.Locale').getDefault().getLanguage() in ('ru', 'uk', 'be'): import lang.ru as lang else: import lang.en as lang GuessTheQuote.lang = lang
Статическая переменная использована для того, чтобы языковые константы было удобно использовать в kv-файле:
app.lang.some_phrase
Это, в общем-то, довольно тривиально, а основное, о чём я хотел рассказать в аспекте локализации — как задать константы в res/values/strings.xml и отдельных локализациях. Зачем это нужно? Как минимум, чтобы задать название приложения на разных языках, а также чтобы прописать такие константы, как app_id для сервисов Google Play и facebook_app_id для сервисов Facebook.
По-умолчанию P4A генерирует strings.xml следующего содержания:
Guess The Quote 1517538478.81 #2EBCB2 kivy
При этом название приложения и цвет фона экрана загрузки можно задать в buildozer.spec. На первый взгляд, этого достаточно, но это только в том случае, если приложение одноязычное, и дополнительные строковые константы не нужны, а это как-то минималистично 🙂 Конечно, никто не запрещает вручную прописать всё необходимое, но при следующей сборке оно затрётся. Также можно вручную создать папки с локализациями, например, values-ru, но они при новых сборках они не будут обновляться. Поэтому лучше ещё раз подправить P4A, а именно, файл .buildozer/android/platform/build/dists/guessthequote/build.py следующим образом:
# оригинальный код, в текущей версии P4A начинается на строке 370 render( 'strings.tmpl.xml', 'res/values/strings.xml', args=args, url_scheme=url_scheme, ) # заменяем на усовершенствованный :) local_args = for key in local_args: local_args[key].name = u'Угадай цитату!' # ну захардкодил, да, ну не готов я пока сделать свой форк P4A и buildozer, чтобы сделать это через передачу параметра for i in os.listdir('res'): if i[:6] == 'values': render( 'strings.tmpl.xml', 'res/'+i+'/strings.xml', args=(args if i == 'values' else local_args[i[7:10]]), url_scheme=url_scheme, ) # и ещё один фрагмент, в текущей версии P4A начиная со строки 388 with open(join(dirname(__file__), 'res', 'values', 'strings.xml')) as fileh: lines = fileh.read() with open(join(dirname(__file__), 'res', 'values', 'strings.xml'), 'w') as fileh: fileh.write(re.sub(r'"private_version">[0-9\.]*<><'.format( str(time.time())), lines)) # тоже заменяем на аналогичный в цикле for i in os.listdir('res'): if i[:6] == 'values': with open(join(dirname(__file__), 'res', i, 'strings.xml')) as fileh: lines = fileh.read() with open(join(dirname(__file__), 'res', i, 'strings.xml'), 'w') as fileh: fileh.write(re.sub(r'"private_version">[0-9\.]*<><'.format( str(time.time())), lines))
Ну а все необходимые вам строковые константы нужно прописать в файле .buildozer/android/platform/build/dists/guessthequote/templates/strings.tmpl.xml
Продолжение следует
Если статья покажется сообществу интересной, во второй части я опишу самые интересные вещи: покупки в приложении, интеграцию сервисов Google Play Games и Facebook SDK, и подготовку release version с последующей публикацией в Google Play, а также подготовлю проект на Github с модулями для реализации описанных задач. Если вам интересны ещё какие-то подробности — напишите в комментариях, постараюсь по возможности осветить.