print
надо использовать FrameBuffer: fb.text(s, x, y); pyb.SCREEN().show(fb)
– с явным заданием координат, без автоматического разбиения на строки и без автоматической прокрутки экрана. Для разработки на Си всё это естественно, но программисты на Python привыкли к намного большему комфорту.Осознавая это, в 2017 нью-йоркский стартап Adafruit начал разработку собственного форка MicroPython, получившего название CircuitPython. Эта реализация выполняет main.py каждый раз, когда файл с таким названием копируется через USB, и затем «обнуляет» среду, так что переменные из main.py не мешают ни REPL, ни следующему запускаемому скрипту. Чтобы остановить выполнение скрипта, достаточно удалить main.py через USB. В MicroPython нужно проявлять осторожность, чтобы во флеш-память не писали одновременно скрипт и компьютер через USB, иначе файловая система могла повредиться. В CircuitPython же перезапись main.py во время его выполнения – основной сценарий использования, так что ради предосторожности нужно выбрать один из двух вариантов – доступ ко флешу из Python только на чтение и через USB на чтение и запись, либо из Python на чтение и запись и через USB только на чтение, без возможности перезаписи либо удаления main.py. И ещё одна приятная фича – что до перехода в графический режим стандартный вывод дублируется и на последовательный интерфейс, и на экран. С другой стороны, MicroPython позволяет писать на Python обработчики прерываний (с рядом ограничений – например, в них нельзя создавать/удалять объекты на куче и нельзя бросать исключения), а CircuitPython – не позволяет, и не собирается добавлять такую
возможность.
Разница в подходе проявляется и в API. MicroPython стремится к краткости, CircuitPython – к абстракции:
MicroPython | CircuitPython |
---|---|
|
|
|
|
|
|
Bitmap
произвольной цветовой глубины, позволяющий сэкономить память, когда одновременно используемых цветов не так много; Palette
, превращающая значения Bitmap в конкретные значения цвета, задаваемые в стандартном 24-битном формате, и автоматически конвертируемые в тот формат, с которым работает экран; затем TileGrid
, позволяющая сдвигать и масштабировать несколько спрайтов как одно целое; и наконец Group
, позволяющая переключаться между «стопками спрайтов». Для простых задач, типа отрисовки графика функции, все эти дополнительные абстракции совершенно лишние; но для разработки игр, скорее всего, программисту на MicroPython пришлось бы самостоятельно разрабатывать нечто аналогичное этой иерархии абстракций.Самое удивительное в CircuitPython – то, что он занимает не больше памяти, чем MicroPython:
свежезагруженной в MeowBit программе остаётся для работы 55 КБ и 53 КБ соответственно. Одна из причин – то, что большинство стандартных модулей CircuitPython ожидаются во флеш-памяти отдельными файлами, и не загружаются, пока не востребованы. (Таков, например, модуль
adafruit_framebuf
, предоставляющий интерфейс стандартного framebuf
из MicroPython.) Полный набор стандартных внешних модулей занимает больше 2 МБ и даже не помещается целиком во флеш-память MeowBit.Один из моментов, вызванный разницей в подходах, хотелось бы разобрать подробнее: сложно представить игру без музыки и/или звуковых эффектов, но если на время проигрывания звуков игра будет приостанавливаться (как в примерах выше с
delay
и sleep
), то играть будет очень неудобно. Как же реализовать фоновый звук в двух вариантах Python?В MicroPython можно напрямую пернести напрашивающееся низкоуровневое решение – обрабатывать прерывание от таймера: функция
play
будет добавлять записи в список music
, а обработчик handle_music
будет обрабатывать их по одной. Ограничения MicroPython не позволяют укорачивать список music
прямо в handle_music
по мере обработки записей, так что придётся пользоваться более низкоуровневыми средствами: продвигать в обработчике указатель next
, и удалять из списка обработанные записи лишь при следующем вызове play
.# `tim` и `ch` как в примере выше music = [] countdown = 0 next = 0 # понимает подмножество синтаксиса QBasic PLAY: # https://en.wikibooks.org/wiki/QBasic/Appendix#PLAY def play(m): global music, next music = music[next:] next = 0 octave = 1 duration = 75 n = 0 while n < len(m): note = m[n] if note >= 'A' and note <= 'G': freq = [440, 494, 262, 294, 330, 349, 392][ord(note)-ord('A')] music.append((freq * 2 ** (octave-1), duration * 7 / 8)) music.append((0, duration / 8)) elif note == 'O': n += 1 octave = int(m[n]) elif note == 'L': n += 2 l = int(m[n-1:n+1]) duration = 1500 / l n += 1 def handle_music(t): global countdown, next if countdown: countdown -= 1 if countdown: return ch.pulse_width_percent(0) if next < len(music): (freq, countdown) = music[next] next += 1 if freq: tim.freq(freq) ch.pulse_width_percent(50) bg_tim = Timer(1, freq=1000) bg_tim.callback(handle_music)
CircuitPython же не позволяет писать обработчики прерываний, так что понадобится намного более высокоуровневая реализация.
handle_music
из повторно вызываемого обработчика превращается в генератор – это ещё и упрощает логику кода: включение динамика, задержка, и выключение динамика теперь идут в коде последовательно, так что можно обойтись без глобального countdown
. Кроме того, генератор может сам удалять из music
обработанные записи, так что упрощается и функция play
.# `pwm` как в примере выше def sleep(duration): until = monotonic_ns() + duration while monotonic_ns() < until: yield def handle_music(): global music while True: if music: (freq, d) = music[0] music = music[1:] if freq: pwm.frequency = int(freq) pwm.duty_cycle = 2**15 yield from sleep(d * 1.e6) pwm.duty_cycle = 0 yield
Но теперь фоновый звук будет проигрываться не сам собой, а только при регулярном «дёрганьи» генератора. Это склоняет к тому, чтобы и остальные игровые процессы реализовать
в виде генераторов; например, заставка игры, прокручивающаяся вверх-вниз до нажатия любой
кнопки, реализуется следующим образом:
play("L08CDEDCDL04ECC") def scroll(): while True: while splash.y > max_scroll: splash.y -= 1 yield from sleep(3.e8) while splash.y < 0: splash.y += 1 yield from sleep(3.e8) def handle_input(): while all(button.value for button in buttons): yield for _ in zip(scroll(), handle_input(), handle_music()): pass
С одной стороны, реализация на MicroPython даёт заметно более качественный звук, потому что обработчик прерывания вызывается точно в заданное время, тогда как в CircuitPython на время перерисовки экрана (порядка 0.15 с) звук «подвисает». С другой стороны, код на CircuitPython легче писать, легче отлаживать и легче расширять, а реализация игровых процессов в виде сопрограмм-генераторов естественна и в отрыве от требований ко звуку.