Pull to refresh

Ну, Pebble, погоди

Reading time 9 min
Views 38K
image
У меня появились очередные умные часы. Pebble.
Поначалу я хотел их отдать коллегам-гаджетоманам, не распаковывая. Ведь мои руки еще помнят часы будущего от Google и Sony. Ничего, кроме сыпи и грусти они не вызывали, хотя дизайн Sony SmartWatch был чудесным.

Ладно, думаю, один вечер поношу Pebble на левой руке. Правая рука у нас для мышки. Часы не раздражали. Я не раздражался. Мало того, в часах открылась чудесная дверь, а за дверью — клад. Натуральное SDK без дураков. То есть человек управляет устройством, а не наоборот. Старомодный язык С и черно-белый экран — разве это не чудо!? Никаких ненавистных REST, паттернов, репозиториев и unit-test-ов. Помолодев на 30 лет, я сделал три приложения и написал маленький обзор рыжего устройства и процесса программирования для Pebble.

Под кнопкой 7 картинок, 7 кусков кода, 7 ссылок и 7 вредных советов.


1. Часы поддерживаются iOS и Android


Для синхронизации часов с iPhone Вы олжны скачать соответствующее приложение Pebble из Appstore.
image
Приложение простое и ясное, объяснений не требует. Недостаток один — нет бесплатной версии под iPad. Недостаток легко обходится при наличии 3 долларов. Продвинутый пользователь при помощи приложения легко может обновить прошивку часов на самую новую. А разработчик через приложение может загрузить на часы программу, созданную своими руками.

2. Как это сделать?


Существует совершенно феноменальная среда разработки приложений для Pebble. Он-лайн разработка — это чудо. Вам не надо скачивать тонны софта, запускать конфигураторы, настройки и звать специалистов из АйТи отдела.

image

Просто зайдите на сайт, зарегистрируйтесь и нажмите кнопку Создать проект. Все! Вы — разработчик приложений для 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.

image
  • Выбираем наш проект, шмем Run Build.
  • Нажимаем сформировавшуюся на странице ссылку — Download compiled PBW.
  • После этого появляется новая страница с надписью — Открыть в Pebble. Жмем и наслаждаемся результатом.


Исполняемые файлы для Pebble имеют расширение PBW. Именно их можно загружать в соответствующий магазин.

4. Разработка игр


Существует два типа приложений для Pebble. Собственно часы (watchface) и приложения (watchapp).
Из чего надо исходить при написании приложений-игр?
Из размеров экрана и числа кнопок. На часах есть три кнопки на правом боку и 1 кнопка на левом боку.
Размер экрана 144 на 168 точек, но верхний statusbar неубираем и крадет 144 на 16 точек пространства.

image

Я, разумеется, создал несколько игр. Тетрис идеально подходит для трех кнопок. Ориентацию экрана я повернул на 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. Магазин приложений


Регистрируйтесь на сайте и выкладывате приложения.

image

Я одно свое выложил, вдруг здесь на Хабре есть обладатели чудного Pebble.
Спасибо, что прочитали.

ЗЫ. Мой приятель скачал приложение и чуть-чуть не добрал до 200 яиц. А кто наберет 200 яиц, тот увидит…
Tags:
Hubs:
+79
Comments 24
Comments Comments 24

Articles