Программируем под Pebble. Урок второй: Камешек, дающий ответы, игральные кости и секс-кубики

Программируем под Pebble. Урок второй: Камешек, дающий ответы, игральные кости и секс-кубики

Часы — это конечно хорошо, но ими забит весь сайт. Надо сделать что-то более интересное.

Помните шарик из «трассы 60»? Давайте сделаем его аналог — приложение, дающее ответ на вопрос.

Шарик Камешек дающий ответы

Только у нас будет не шарик, а камешек, то есть Pebble :) Что нам для этого надо? Список ответов и генератор случайных чисел. Список ответов мы возьмем на википедии. Создадим массив и заполним его ответами: Порядок не важен, все равно они будут выбираться из него случайных образом.

Создаем главное окно программы и текстовый слой так же, как и в предыдущем уроке, за исключением небольшой разницы — функцией window_set_fullscreen() мы убираем верхний бар приложения. Вызывается она так: Первый агрумент — имя окна, второй, соответственно true — полный экран, false — c баром. Тонкость — эта функция должна вызываться до window_stack_push, иначе чуда не произойдет.

Теперь займемся генератором случайных чисел. Для этого существует функция rand(), которая возвращает при каждом вызове случайное число. Как и любой программный ГСЧ, она нуждается в инициализации случайным числом перед началом работы, иначе строчка цифр будет повторяться при каждом запуске. Делается это функцией srand(). Например в SDK есть пример, где она инициализируется текущим временем. Так как мы не знает, в какое время программа запустится, и это время каждый раз разное — то это достаточный источник энтропии для нашей идеи. Делаем вот так:

Правда, генератор теперь может выдать довольно большое число — заведомо больше, чем у нас возможных вариантов ответов. Поэтому, при применяем вот такую конструкцию messages[rand()%21] . % — это остаток целочисленного деления. Допустим, генератор возвращает 456, они делятся на 21, а остаток(456-21*21) — 15 мы используем в качестве номера ответа. Он не может быть больше 21, потому что при этом остаток от деления будет равен нулю.

Объединяем все вышесказанное в одно целое, и у нас получается вот такой вызов функции: Вот весь исходник:

Программа выводит сообщение при запуске и больше не делает ничего. Для показа другого сообщения надо перезапустить программу:

Следующий шаг — сделаем вывод сообщения при нажатии на кнопку.

В API Pebble применяется термин «подписка» для назначения функций, которые будет запущены не в главном цикле, а после каких-то действий — тика таймера, нажатия кнопки и так далее. Помните, в прошлом уроке мы подписывались на таймер, который раз в секунду вызывал функцию обновления экрана? Тут нечто похожее, мы подписываемся на нажатие кнопки(причем в API уже есть несколько готовых вариантов — одиночное нажатие, двойное нажатие, удержание и так далее), после нажатия которой будет запущена заданная нами функция.

API говорит о том, что нам надо вызвать метод window_set_click_config_provider(), которая в качестве аргументов примет указатель на окно, в котором надо ловить нажатия, и название функции, в которой будет описаны подписки. Обратите внимание, подписываться на кнопки мы будет не тут, а только в функции, указанной в window_set_click_config_provider. Делается это вот так: Указатель окна — window, название функции — WindowsClickConfigProvider. Создаем ее:

И добавляем внутрь методы window_single_click_subscribe, принимающие аргументами название кнопки и функцию, в которую будет передано управление: В данном случае при нажатии кнопки вверх, мы вызываем click. Кнопки(их у нас 3) называются соответственно BUTTON_ID_UP, BUTTON_ID_SELECT, BUTTON_ID_DOWN. Подписаться на кнопку «назад» можно, но она всегда будет выбрасывать вас на предыдущий экран(в случае, если у приложения один экран — в меню).

Теперь приложение выглядит вот так:

Работает! Можно присваивать версию 0.1 :) Но чего-то не хватает. Хорошо бы добавить каких-нибудь эффектов. Можно сделать анимацию изменения ответов, чтобы часы отвечали не сразу при нажатии на кнопку, а несколько секунд «перебирали» варианты. В кавычках — потому что ответ будет случайным в любом случае, но в первом случае у пользователя будет больше доверия к приложению — все мы знакомы с случайным процессом выбора карточек из мешка или шаров с цифрами при игре в лотерею.

Еще более красиво будет, если мы добавим немного физики. Дело в том, что люди, сами того не подозревая, окружены процессами протекающими по обратной экспоненте — когда процессы «быстрые» в самом начале, проходят не линейно, а теряя скорость тем сильнее, чем больше времени прошло с начала. Это может быть что угодно — например, остывание горячего чая на столе. Когда разница температур чая и окружающего воздуха велика, он остывает гораздо быстрее, чем когда он чуть теплый. Или пузырьки в стакане с газированной водой. Сначала, когда концентрация угольной кислоты в воде велика, она разлагается очень активно, но через некоторое время концентрация уменьшается, а вместе с ней уменьшается и количество пузырьков. В данном случае я бы хотел сделать что-то вроде колеса фортуны — там, где сначала сегменты сменяют друг друга быстро, постепенно замедляясь. Тут «виновато» трение — чем больше скорость, тем больше колесо трется об ось и тем больше теряет энергию, переводя ее в тепло и замедляясь. Но при уменьшении скорости уменьшаются потери и на трение — в итоге 50% своей скорости колесо теряет в первые 20% времени, а остальные 50% — за оставшиеся 70%.

Отсчитывать время мы будем в миллисекундах между сменами сообщений. Путем опытов, было выяснено, что хороший диапазон значений — от 0 до 300-400мс. Если промежутки больше — возникает ощущение, что уже показанный ответ внезапно поменялся. Наиболее простая функция, которая обеспечит такое поведение — это что-то типа x=x*2. Но при умножении на 2 график функции поднимается слишком резко, каждую итерацию время задержки увеличивается в два раза. Мы получим 256(максимальную задержку, поскольку следующее значение — 512 уже выходит за границы удобства) уже на 8 шаге с длительностью работы всего в 1+2+4+8+16+32+64+128+256=511мс. Пол-секунды это слишком быстро. Опытным путем я понял, что множитель должен быть в районе 1.08-1.2, тогда мы получаем около 30 шагов и длительность около 3 секунд. Но мне не нравится функция x=x*y — она слишком полого поднимается, и мелькание замедляется слишком медленно(да, я странный), хотелось бы, чтобы оно дольше мелькало «быстро», а потом резко остановилось.

Можно было поступить проще — забить значение задержек в массив и втирать править, править их до удовлетворения, благо их не так и много — штук 40. Но гораздо интереснее вывести функцию, которая бы работала нужным нам образом. Раз умножение нам не подходит, попробуем деление. Что-то вроде x=x/0.7. Но сама по себе она слишком резко возрастает — 300 мы получаем уже на 18 шаге, а хотелось бы чуть больше. Но можно разделить например, на 100. Или на 1000. Построим графики всех функций при начальном значении 1. Цифры над графиками — количество шагов.

Вот, значит нам вполне подходит y=y/0.7 x=y/100, можно делать.

Но для начала — исправим один глюк: т.к. информация при запуске и сообщения выводятся у нас в одном и том же слое, то сообщения показываются в верхней части экрана(выравнивания по вертикали, к сожалению, нет). Да еще и мелким шрифтом, хотя их можно сделать и больше — они гораздо короче и поместятся в экран. Я не нашел в API методов изменения размеров слоя, поэтому нам придется удалить слой и создать его опять, но с другими координатами. Однако, при этом получится некрасиво — часть кода будет дублироваться(настройки слоя после его создания), чтобы этого не происходило — вынесем часть кода в отдельную функцию. Создаем: Первые 4 параметра — это информация о слое — координаты точки и размеры, пятый параметр — это шрифт. Соответственно, перепишем функции инициализации для использования этих переменных:

И вставим их в нашу функцию вместе с остальным кодом настройки слоя. Теперь можно просто сделать в нужном месте вот так:

Так и сделаем в функции click, которая вызывается у нас при нажатии кнопки. Да, слой будет создаваться каждый раз при нажатии кнопки, избежать этого можно простой проверкой, что-то типа: Но я не стал заморачиваться. Лучше займемся таймером. Поиск в API дал функцию app_timer_register(), которая принимает в качестве аргументов значение в мс, через которое сработает таймер, имя функции, которую надо вызвать при его срабатывании и указатель на данные, которые надо передать в эту функцию. К сожалению, как работать с указателем я не разобрался, так что придется плюнуть на красоту кода и сделать через глобальную переменную:

Создаем функцию, которая будет рекурсивно вызывать таймер со все возрастающими интервалами.

Центром функции является переменная с плавающей точкой timer_delay. Она делится на 0.7, пока не достигнет 30000. Каждое новое значение переменной делится на 100 и отдается в качестве аргумента задержки функции app_timer_register, которая при срабатывании опять вызовет эту функцию.

Осталось только добавить вызов этой функции в click():

И можно наслаждаться результатом:

Если немного подумать, что у нас получилось, то окажется, что мы создали платформу для симуляции любых штук, которые используются в реальном мире как ГСЧ. Сложное движение многогранной фигуры в жидкости внутри шарика — процесс, не поддающийся предсказанию. Так же как и игральные кости. Кстати, игральные кости! Чем не вариант для еще одного приложения? Я действительно полез искать рисунки на гранях костей и наткнулся на… нет, ну это тоже рисунки. И тоже на игральных костях. Только не точечками. В общем, я наткнулся вот на это:

И все. Какие там игральные кости, когда тут есть такая замечательная идея. Пошли реализовывать!

Секс-рулетка

Логика работы программы остается прежней. Меняем массив messages:

И сообщение при запуске:

Создаем в памяти, на которую указывает указатель графический массив, указывая координаты. Синтаксис абсолютно такой же, как и в text_layer_create:

Делаем созданный слой ребенком слоя главного окна. Зачем это нужно? Это настраивает «высоту»(в терминах css, если кому понятнее — z-index) слоя по отношению к другим слоям, от этого зависит то, как слои будут друг друга перекрывать. Все дети находятся выше всего родителя и закрывают его своим выводом.

Настраиваем режим наложения. В зависимости от этого параметра картинка будет по разному взаимодействовать с фоном.

Все варианты можно описаны тут, или можно повтыкать в картинку:

Source — это в данном случае графический слой, а Destination — его родитель, главный слой окна. Как все просто и замечательно укладывается в 6 вариантов при работе с монохромными картинками…

Теперь займемся выводом рандомных картинок. Поищем в гугле подходящие картинки(двухцветные и маленькие) по какому-нибудь запросу типа "иконки позы". На этом сайте нашлось вот такое:

Скачиваем вектор, удаляем всякие границы, экспортируем в PNG разрешением повыше, открываем в каком-нибудь графическом редакторе. Выделяем каждую картинку, удаляем неудачные и непонятные, вписываем ее в квадрат, уменьшаем до 144 пикселей, подчищаем недостатки. Итогом нашей работы должна стать папка с картинками:

Из одной из картинок делаем логотип для меню, размером 24x28 пикселей: ->

Открываем файл appinfo.json в папке с проектом и добавляем в него наши созданные ресурсы. К сожалению, не могу рассказать как это делается в CloudPebble — она внезапно перестала у меня работать, не принимая пароль и зажав исходники всех моих проектов. Так что я поставил себе среду разработки на компьютер по инструкции с официального сайта, и пишу там.

Так вот, о appinfo.json. Он выглядит вот так: В начале идут знакомые вам поля, смысл которых понятен и без объяснений, а дальше в нем описываются все 32 картинки. Обратите внимание, структура должна быть именно вот такая:

После последней скобки не должно быть запятой, как и после указания адреса файла. А поля должны следовать именно в порядке type, name, file. Что, блин, идет вразрез с официальной документацией:

Клавиатуру в жопу запихать! Посылаю лучики поноса тем, кто писал эту документацию, я полчаса тупил и гадал, на что же он ругается. Еще и ругается так невнятно, что толком не поймешь, что его не устраивает: Ну ладно, хрен с ним. Как вы уже поняли, картинка-иконка для меню помечается полем «menuIcon»: true, причем к ней тоже можно обращаться из программы.

Вернемся к нашим картинкам. Документация предлагает загружать картинку вот так:

Что для нас не подходит. Не городить же монструозную конструкцию вида

Это некрасиво. Из документации мы понимаем, что RESOURCE_ID_POSE_2 — это всего лишь переменная типа uint32_t в которой хранится номер ресурса.

Создаем массив нужного нам типа:

И перечисляем в нем нужные нам ресурсы:

Теперь мы можем вызвать случайную картинку так же, как и текст:

Мы вызвали картинку из небытия из флеш-памяти и разместили ее в оперативке. Теперь надо передать указатель на место хранение изображения функции bitmap_layer_set_bitmap, которая и отобразит ее на нужном нам графическом слое. Делается это вот так:

image_layer — это указатель на кусок памяти графического слоя, а image — указатель на картинку в памяти. Вроде все. Запускаем!

Часы зависли, потом перезагрузились, а после второго запуска ушли в Recovery и попросили перепрошиться. Правильно, мы, загружая в каждом цикле картинку в память, не выгрузили ее оттуда. Забили оперативку, и залезли куда-то еще, судя по тому, что часы отказались загружаться. Это spaaarta! embeeeeedded! Тут такое не прощают. Делаем правильно. Сначала очищаем память, а потом загружаем в нее новую картинку для показа.

Но при самом первом клике у нас в указателе image ничего нет, и gbitmap_destroy обязательно это обнаружит. App Crashed… Можно во время инициализации программы подсунуть ему туда картинку, чтобы было что удалять, но это как-то некрасиво. Похоже, без флага первого запуска нам все-таки не обойтись. Создаем переменную:

Сбрасываем ее при первом запуске функции, вызываемой таймером:

И оборачиваем gbitmap_destroy — в проверку на first_time == false, а text_layer_destroy и config_text_layer в функции click — соответственно, в проверку на first_time == true:

Нет, стоп. Посмотрим внимательно на последние строчки нашей программы:

В числе прочих мы уничтожаем gbitmap. Но если мы запустим программу и тут же выйдем, он не будет создан! Уничтожать нам еще нечего, и при выполнении этой функции программа упадает и потянет за собой лаунчер — часы перезагрузятся. А это нехорошо. Раз уж у нас есть флаг первого включения — можно использовать его, проверяя перед уничтожением ресурсов:

Вот теперь точно все.

А игральные кости?! Придется сделать.

Игральные кости

Берем за основу предыдущую программу. Картинки мы возьмем из википедии. Уменьшим их до размера 75х75 пикселей и сделаем иконку:

Подключим их в appinfo.json, так же как и в предыдущем примере, и опишем их в массиве images:

Удалим из программы обработку нажатий: WindowsClickConfigProvider и click. Это же кубики, их надо трясти! Будем использовать акселерометр.

Подписываемся на события от акселерометра:

Создаем функцию, которая будет вызываться при встряхивании часов:

Логика ее работы проста: если у нас на экране еще есть текстовый слой(мы встряхиваем часы в первый раз после запуска) — удаляем его. Если нет — то не удаляем :) И в любом случае запускаем перебор.

Теперь изменяем функцию timer_call. Картинка у нас меньше экрана(я пробовал делать картинку кубика такой же, как в прошлом разе — 144х144, но смотрелось это не очень), поэтому нам ее надо двигать по экрану. Двигать мы будем простым способом — удаляя и создавая новую в случайном месте. Используем для этого мы опять любимую функцию rand. У нас есть картинка 75х75 пикселей и экран 144х168. Так как мы указываем при создании слоя его верхний левый угол, то, чтобы картинка не попала за край экрана, нам надо указать его в диапазоне 0. 69(144-75) для х-координаты, и 0. 93(168-75) для y-координаты. Делаем это вот так: rand()%(144-75) и rand()%(168-75). В итоге создание слоя выглядит вот так:

Еще меняем способ расчета задержки: Если в прошлом варианте у нас был множитель 100, то в этом — 1000. Это опять же сделано из соображений эстетики. Как мы видим из графика, такой множитель почти не изменяет скорость возрастая функции, но добавляет немного шагов. Дело в том, что во время движения рукой — начало смены картинок плохо видно, и чтобы увидеть то, ради чего затевалась эпопея с таймером — красивую смену картинок, надо немного продлить работу функции.

Дальше все так же, как и в предыдущем примере. Вот только небольшая проблема: так как мы не нажимаем кнопку, подсветка не загорается. Хорошо бы ее зажечь после движения. Смотрим, что нам предоставляет API для этого:

Не густо. Но нам хватит. Первая функция может зажечь или погасить подсветку постоянно: light_enable(true) или light_enable(false), а вторая — зажигает подсветку, и она сама гасится через некоторое время(как при нажатии на кнопку). Чтобы не городить отдельный таймер для отключения подсветки(мы же не хотим, чтобы она горела постоянно при работе программы?), воспользуемся вторым вариантом. Поместим его куда-нибудь в конец timer_call. До кучи — в main поправим сообщение на начальном экране:

И при выходе из программы поменяем логику удалениия ресурсов и добавим отписку от акселерометра: Тут мы делаем вот что. Если выходим сразу после запуску — с начального экрана, то нам надо удалить только текстовый слой. Если мы выходим уже после запуска — после того, как часы потрясли — то нам надо удалить графический слой и память под картинку, а текстовый слой не надо — он уже удален при срабатывании таймера в функции accel_int

📎📎📎📎📎📎📎📎📎📎