У меня появились очередные умные часы. Pebble.
Поначалу я хотел их отдать коллегам-гаджетоманам, не распаковывая. Ведь мои руки еще помнят часы будущего от Google и Sony. Ничего, кроме сыпи и грусти они не вызывали, хотя дизайн Sony SmartWatch был чудесным.
Ладно, думаю, один вечер поношу Pebble на левой руке. Правая рука у нас для мышки. Часы не раздражали. Я не раздражался. Мало того, в часах открылась чудесная дверь, а за дверью — клад. Натуральное SDK без дураков. То есть человек управляет устройством, а не наоборот. Старомодный язык С и черно-белый экран — разве это не чудо!? Никаких ненавистных REST, паттернов, репозиториев и unit-test-ов. Помолодев на 30 лет, я сделал три приложения и написал маленький обзор рыжего устройства и процесса программирования для Pebble.
Под кнопкой 7 картинок, 7 кусков кода, 7 ссылок и 7 вредных советов.
1. Часы поддерживаются iOS и Android
Для синхронизации часов с iPhone Вы олжны скачать соответствующее приложение Pebble из Appstore.
Приложение простое и ясное, объяснений не требует. Недостаток один — нет бесплатной версии под iPad. Недостаток легко обходится при наличии 3 долларов. Продвинутый пользователь при помощи приложения легко может обновить прошивку часов на самую новую. А разработчик через приложение может загрузить на часы программу, созданную своими руками.
2. Как это сделать?
Существует совершенно феноменальная среда разработки приложений для Pebble. Он-лайн разработка — это чудо. Вам не надо скачивать тонны софта, запускать конфигураторы, настройки и звать специалистов из АйТи отдела.
Просто зайдите на сайт, зарегистрируйтесь и нажмите кнопку Создать проект. Все! Вы — разработчик приложений для Pebble. Наслаждайтесь языком С и очень похожей на Windows 3.0 парадигмой рисования битмапов, текста, обработки нажатий на три правые кнопки и одну левую. Есть вибрация и функция ее вызова. Звука нет, но я люблю тишину.
В установках проекта выбор небольшой.
Существует два типа приложений для Pebble. Собственно часы (watchface) и приложения (watchapp). Я выбрал второй вариант.
Существует две версии OS (как и версий SDK) для часов. 1.x и 2.x. Я выбрал разработку под вторую версию — официальный релиз 2.0 обещают к Новому Году. Проекты без изменения можно пересобрать и под 1.x, если Вы не используете крутых возможностей второй версии.
К проекту простым нажатием можно добавлять файлы и ресурсы. Файлы — текстовый код С, *.c и *.h. Ресурсы — картинки в формате битовых PNG и шрифты.
Что дальше?
На сайте Pebble разработчиков Вам пригодится готовый набор проектов-примеров. Он находятся в Pebble SDK- скачивать можно только после регистрации. Примеров около 50-ти и они чудесны.
3. Загрузка приложения на часы
Загрузка приложения на часы осуществляется в три клика при помощи iPhone, синхронизированного с часами.
Заходим через Safari в среду разработки cloudpebble.net с Вашего iPhone/iPad.
- Выбираем наш проект, шмем Run Build.
- Нажимаем сформировавшуюся на странице ссылку — Download compiled PBW.
- После этого появляется новая страница с надписью — Открыть в Pebble. Жмем и наслаждаемся результатом.
Исполняемые файлы для Pebble имеют расширение PBW. Именно их можно загружать в соответствующий магазин.
4. Разработка игр
Существует два типа приложений для Pebble. Собственно часы (watchface) и приложения (watchapp).
Из чего надо исходить при написании приложений-игр?
Из размеров экрана и числа кнопок. На часах есть три кнопки на правом боку и 1 кнопка на левом боку.
Размер экрана 144 на 168 точек, но верхний statusbar неубираем и крадет 144 на 16 точек пространства.
Я, разумеется, создал несколько игр. Тетрис идеально подходит для трех кнопок. Ориентацию экрана я повернул на 90 градусов, иначе игра превращается в мучение. Но тетрисы и арканоиды не цепляют. На подобном экране надо делать только Ну, погоди. То самое, с Электроники.
5. Особенности разработки
Сначала я решил разжевать суть программирования под Pebble на примере игры Ну погоди.
А потом подумал, зачем? Весь текст я передрал из примеров. На написание программы ушло два часа, потому прошу простить плохой код. Меня оправдывает скорость и то, что он работает. Файл в проекте один, да именно main.c.
#include "pebble.h" static int egg_status[8]; static int egg_ticks[8]; static int egg_places[]= { 4, 0, 0, 0, 0, 2, 5, 0, 0, 0, 2, 4, 6, 0, 0, 1, 3, 4, 6, 0, 1, 2, 4, 5, 6, 0 }; static int level = 0; static int levelFlag = 0; static int egg_pos[]= { -2, 72, 0, 75, 4, 78, 9, 83, 14, 88, 20, 94, 0, 0, -2, 22, 0, 25, 4, 28, 9, 33, 14, 38, 21, 44, 0, 0, 137, 30, 134, 34, 130, 38, 127, 43, 121, 48, 114, 54, 0, 0, 137, 83, 134, 86, 131, 89, 127, 94, 121, 99, 114, 104, 0, 0 }; static int current_pos = 1; static int best = 25; static AppTimer *timer; static Window *window; static Layer *layer; // We will use a bitmap to composite with a large circle static GBitmap *image_1; static GBitmap *image_2; static GBitmap *image_3; static GBitmap *image_4; static GBitmap *image_5; static GBitmap *image_7; static GBitmap *image_8; static GBitmap *image_6; static GBitmap *image_0; static int ticks = 0; static int score = 0; static int failed = 0; static int egg_failed = 0; char *itoa(int num); static const VibePattern broken_egg_pattern = { .durations = (uint32_t []) {50, 50, 50}, .num_segments = 3 }; static const VibePattern game_over_pattern = { .durations = (uint32_t []) {300, 100, 300}, .num_segments = 3 }; static void timer_callback(void *context) { if (failed<3) { levelFlag = 0; egg_failed = 0; for (int i=0; i<8; i++) if (egg_status[i]>0) { egg_ticks[i]++; if (egg_ticks[i]==6) { int k = egg_status[i]; if (k==current_pos) { score++; } else { egg_failed = k; ++failed; int game_over = failed == 3; //TODO: endgame screen if (game_over) { vibes_enqueue_custom_pattern(game_over_pattern); } else { vibes_enqueue_custom_pattern(broken_egg_pattern); } } egg_ticks[i] = 0; egg_status[i] = 0; } } int lev = 1; if (score>10) lev = 2; if (score>10*3) lev = 3; if (score>10*10) lev = 4; if (lev!=level) { level = lev; levelFlag = 1; vibes_enqueue_custom_pattern(broken_egg_pattern); } for (int i =0; i<level; i++) { int k = egg_places[i+(level-1)*5]; int j = ticks%8; if (k==j) { egg_ticks[i] = 0; egg_status[i] = 1 + rand()%4; } } if (levelFlag) { ticks=0; for (int i =0; i<8; i++) { egg_status[i] = 0; egg_ticks[i] = 0; } } else { ticks++; } } layer_mark_dirty(layer); const uint32_t timeout_ms = 500; timer = app_timer_register(timeout_ms, timer_callback, NULL); } // This is a layer update callback where compositing will take place static void layer_update_callback(Layer *layer, GContext* ctx) { char title[20]; GRect bounds = layer_get_frame(layer); GRect destination = image_1->bounds; destination.origin.x = (bounds.size.w-destination.size.w)/2; destination.origin.y = 30; if (failed<3) { if (levelFlag) { snprintf(title, 20, "\nLevel \n%d", level); // Display the name of the current compositing operation graphics_context_set_text_color(ctx, GColorBlack); graphics_draw_text(ctx, title, fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD), bounds, GTextOverflowModeTrailingEllipsis, GTextAlignmentCenter, NULL); } else { snprintf(title, 20, "%d/%d", score, best); // Display the name of the current compositing operation graphics_context_set_text_color(ctx, GColorBlack); graphics_draw_text(ctx, title, fonts_get_system_font(FONT_KEY_GOTHIC_24_BOLD), bounds, GTextOverflowModeTrailingEllipsis, GTextAlignmentCenter, NULL); // strcpy(title, "Lev "); // strcat(title, itoa(level)); // Draw the large circle the image will composite with // graphics_context_set_fill_color(ctx, GColorBlack); // graphics_fill_circle(ctx, GPoint(bounds.size.w/2, bounds.size.h+110), 180); // Use the image size to help center the image // Center horizontally using the window frame size // Set the current compositing operation // This will only cause bitmaps to composite // Draw the bitmap; it will use current compositing operation set if (current_pos==1) graphics_draw_bitmap_in_rect(ctx, image_1, destination); if (current_pos==2) graphics_draw_bitmap_in_rect(ctx, image_2, destination); if (current_pos==3) graphics_draw_bitmap_in_rect(ctx, image_3, destination); if (current_pos==4) graphics_draw_bitmap_in_rect(ctx, image_4, destination); GRect ground = image_0->bounds; ground.origin.x = (bounds.size.w-ground.size.w)/2; ground.origin.y = 30; graphics_context_set_compositing_mode(ctx, GCompOpAnd); graphics_draw_bitmap_in_rect(ctx, image_0, ground); for (int i=0; i<8; i++) if (egg_status[i]>0) { int img= egg_ticks[i]%2; int k = (egg_ticks[i]+(egg_status[i]-1)*7) *2; int x = egg_pos[k]; int y = egg_pos[k+1]; GRect egg_destination = image_5->bounds; egg_destination.origin.x = x; egg_destination.origin.y = y; graphics_draw_bitmap_in_rect(ctx, (img) ? image_5 : image_7, egg_destination); } if (egg_failed) { GRect egg_destination = image_6->bounds; egg_destination.origin.x = egg_failed < 3 ? 10 : 101; egg_destination.origin.y = 130; graphics_draw_bitmap_in_rect(ctx, image_6, egg_destination); } } } else { if (score>best) best = score; snprintf(title, 20, "\nLast %d\nBest %d", score, best); // Display the name of the current compositing operation graphics_context_set_text_color(ctx, GColorBlack); graphics_draw_text(ctx, title, fonts_get_system_font(FONT_KEY_GOTHIC_28_BOLD), bounds, GTextOverflowModeTrailingEllipsis, GTextAlignmentCenter, NULL); GRect egg_destination = image_6->bounds; egg_destination.origin.y = 130; egg_destination.origin.x = 10; graphics_draw_bitmap_in_rect(ctx, image_6, egg_destination); egg_destination.origin.x = 56; graphics_draw_bitmap_in_rect(ctx, image_6, egg_destination); egg_destination.origin.x = 101; graphics_draw_bitmap_in_rect(ctx, image_6, egg_destination); } } static void select_click_handler(ClickRecognizerRef recognizer, void *context) { if (failed==3) { level = 0; score = 0; for (int i =0; i<8; i++) { egg_status[i] = 0; egg_ticks[i] = 0; } failed = 0; } layer_mark_dirty(layer); } static void up_click_handler(ClickRecognizerRef recognizer, void *context) { current_pos--; if (current_pos<1) current_pos = 4; layer_mark_dirty(layer); } static void down_click_handler(ClickRecognizerRef recognizer, void *context) { current_pos++; if (current_pos>4) current_pos = 1; layer_mark_dirty(layer); } static void config_provider(void *context) { window_single_click_subscribe(BUTTON_ID_UP, up_click_handler); window_single_click_subscribe(BUTTON_ID_DOWN, down_click_handler); window_single_click_subscribe(BUTTON_ID_SELECT, select_click_handler); } int main(void) { time_t t = 0; uint16_t t_ms = 0; time_ms(&t, &t_ms); srand(t * 1000 + t_ms); // Then use the respective resource loader to obtain the resource for use // In this case, we load the image image_1 = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_WOLF_1); image_2 = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_WOLF_2); image_3 = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_WOLF_3); image_4 = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_WOLF_4); image_5 = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_EGG_1); image_6 = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_EGG_0); image_7 = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_EGG_2); image_8 = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_EGG_3); image_0 = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_GROUND); window = window_create(); window_stack_push(window, true /* Animated */); window_set_click_config_provider(window, config_provider); // Initialize the layer Layer *window_layer = window_get_root_layer(window); GRect bounds = layer_get_frame(window_layer); layer = layer_create(bounds); // Set up the update layer callback layer_set_update_proc(layer, layer_update_callback); // Add the layer to the window for display layer_add_child(window_layer, layer); const uint32_t timeout_ms = 500; timer = app_timer_register(timeout_ms, timer_callback, NULL); for (int i =0; i<8; i++) { egg_status[i] = 0; egg_ticks[i] = 0; } // Enter the main loop app_event_loop(); // Cleanup the image gbitmap_destroy(image_1); gbitmap_destroy(image_2); gbitmap_destroy(image_3); gbitmap_destroy(image_4); gbitmap_destroy(image_5); gbitmap_destroy(image_6); gbitmap_destroy(image_7); gbitmap_destroy(image_0); gbitmap_destroy(image_8); layer_destroy(layer); window_destroy(window); }
Чуть-чуть прокомментирую.
#include "pebble.h" int main(void) { // Then use the respective resource loader to obtain the resource for use // In this case, we load the image image_1 = gbitmap_create_with_resource(RESOURCE_ID_IMAGE_WOLF_1); window = window_create(); window_stack_push(window, true /* Animated */); // Initialize the layer Layer *window_layer = window_get_root_layer(window); GRect bounds = layer_get_frame(window_layer); layer = layer_create(bounds); // Set up the update layer callback layer_set_update_proc(layer, layer_update_callback); // Add the layer to the window for display layer_add_child(window_layer, layer); // Enter the main loop app_event_loop(); // Cleanup the image gbitmap_destroy(image_1); layer_destroy(layer); window_destroy(window); }
В функции main() стандартно вызываются функции инициализации основного окна, загрузка битмапов из ресурсов и именование функций обработки нажатий.
После этого закручиваем бесконечный цикл
app_event_loop();
Как в Windows 3.1.
После бесконечного цикла не забудьте освободить все ресурсы и главное окно.
Все.
Кроме того, доступны все стандартные функции для игроделов, включая rand();
6. Возможности часов версии 2.0
- Начиная с версии 2.0 появилась поддержка акселерометра.
- Начиная с версии 2.0 появилась поддержка сохранения preferences .
- Начиная с версии 2.0 появилась поддержка JSScript — Вы можете сохранять результаты на сервер.
А что там с русскими шрифтами, спросит Mithgol?
Для любителей русского языка есть грязный хак. Я не проверял.
7. Магазин приложений
Регистрируйтесь на сайте и выкладывате приложения.
Я одно свое выложил, вдруг здесь на Хабре есть обладатели чудного Pebble.
Спасибо, что прочитали.
ЗЫ. Мой приятель скачал приложение и чуть-чуть не добрал до 200 яиц. А кто наберет 200 яиц, тот увидит…
ссылка на оригинал статьи http://habrahabr.ru/post/202164/
Добавить комментарий