На Хабре уже было несколько статей об общих принципах написания кода под Pebble. Для программирования используется язык C, а сам процесс разработки происходит в браузере, при этом компиляция происходит на удаленных серверах, и изменить ее параметры нет возможности, разве что установить Ubuntu и инсталлировать необходимые инструменты для офлайн-компиляции. Но даже такой ход не избавит основного ограничения – на устройстве доступно только 24 Кб оперативной памяти, которая используется и для скомпилированного кода, то есть действительно динамической памяти остается 5-10 Кб. И если для простых программ, которые используются как тонкие клиенты или дополнительные датчики для телефона, этого с головой достаточно, то для написания самодостаточной более или менее сложной игры, которой не нужен смартфон, этого откровенно мало. Вот здесь и понадобится оптимизация кода под размер.
Свои шишки я уже набила, и поэтому предлагаю поучиться на моих ошибках, которые я объединила в 16 советов. Некоторые из них могут показаться капитанскими, от некоторых избавит хороший компилятор с правильными флагами компиляции, но, надеюсь, некоторые из них кому-нибудь да и будут полезными.
Вспомнила об этой игре я тогда, когда у меня появился pebble. Его железо на порядок мощнее тех старых телефонов, но экран почти такой же. После простых тестов оказалось, что этот экран вполне может показывать 60 кадров на секунду. Более-менее сложные игры в pebble appstore можно пересчитать на пальцах, поэтому решено было писать Stack Attack на pebble.
Поиск скриншотов, из которых можно было бы получить нормальные ресурсы для игры, ничего не дал. Поэтому я нашла эмулятор Siemens C55 на старом-престаром сайте, и саму игру. Таким образом удалось вспомнить, как же выглядела игра. А после ковыряния в jar-архиве удалось относительно просто выудить картинки и тексты.
Для тех, кто не хочет устанавливать всякие непонятные эмуляторы (которые, как ни странно, даже на Windows 8 с горем пополам запускаются), я записала ностальгические видео:
1. Первый, и самый очевидный способ – используйте inline везде, где есть такая возможность. Если функция вызывается ровно 1 раз, то это позволит сэкономить 12 байт. Но если функция не является тривиальной, то можно и сильно залететь, поэтому будьте осторожны. Также минусом этого способа является то, что в большинстве случаев придется писать код в .h-файле.
2. Как бы банально это не звучало, пишите меньше кода, пока это не мешает его нормально читать. В общем случае, меньше кода – меньше бинарный файл.
3. Перенесите тексты в файлы ресурсов. Программа для pebble может содержать около 70 Кб ресурсов, чего вполне достаточно, если она не показывает новую картинку ежеминутно.
Обычно все тексты не отображаются сразу, поэтому использование динамической памяти вместо статической позволит сэкономить место. Недостатком является то, что придется писать лишний код, который подгружает и выгружает ресурсы по их идентификаторам. Также может показаться, что читабельность кода от этого пострадает, впрочем, это не всегда так. В качестве примера приведу код из своей игры (вверху) и аналогичный участок кода с тестового проекта (снизу):
static void cranes_menu_draw_row_callback(GContext* ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data)
{
int const index = cell_index->row;
menu_cell_basic_draw( ctx, cell_layer, texts[ index * 2 ], texts[ index * 2 + 1 ], NULL );
}
static void menu_draw_row_callback(GContext* ctx, const Layer *cell_layer, MenuIndex *cell_index, void *data) {
switch (cell_index->row) {
case 0:
menu_cell_basic_draw(ctx, cell_layer, "Basic Item", "With a subtitle", NULL);
break;
case 1:
menu_cell_basic_draw(ctx, cell_layer, "Icon Item", "Select to cycle", NULL);
break;
}
}
4. Вместо освобождения каждого из ресурсов отдельно, используйте массивы ресурсов и освобождайте их в цикле. При трех и более ресурсах такой подход позволяет экономить память.
Пример:
for ( int i=0; i<7; ++i ) {
gbitmap_destroy( s_person_images[i] );
}
краще, ніж
gbitmap_destroy( s_person1_image );
...
gbitmap_destroy( s_person7_image );
5. Избегайте лишних переменных там, где это представляется целесообразным. Например, код
for (int i=0; i<9; ++i)
{
for (int k=0; k<3; ++k)
{
btexts[i/3][i%3][k] = master[count];
count++;
}
}
занимает на 20 байт меньше, чем
for (int i=0; i<3; i++)
{
for (int j=0; j<3; j++)
{
for (int k=0; k<3; k++)
{
btexts[i][j][k] = master[count];
count++;
}
}
}
6. Если код уже невозможно прочитать, пишите наиболее оптимально. Например, вот часть кода из проекта tertiary_text:
size /= 3;
if (b == TOP)
end -= 2*size;
else if (b == MID)
{
top += size;
end -= size;
}
else if (b == BOT)
top += 2*size;
Этот код делает то же, что и
size /= 3;
top += b*size;
end -= (2-b)*size;
Верхний и нижний код отличаются по размеру в несколько раз, и, по-моему, читабельность у них одинаково низкая.
7. Используйте enum для того, чтобы перенести последовательные вызовы в цикл. Более того, за счет процессорной магии такой код может работать даже чуть быстрее.
unsigned char RESOURCE_ID_BOXES[11] = { RESOURCE_ID_BOX1, RESOURCE_ID_BOX2, RESOURCE_ID_BOX3, RESOURCE_ID_BOX4, RESOURCE_ID_BOX5,
RESOURCE_ID_BOX6, RESOURCE_ID_BOX7, RESOURCE_ID_BOX8, RESOURCE_ID_BOX9, RESOURCE_ID_BOX10,
RESOURCE_ID_BOX11 };
for (int i=0; i<11; ++i) {
s_boxes_bitmap[i] = gbitmap_create_with_resource( RESOURCE_ID_BOXES[i] );
}
вместо
s_boxes_bitmap[0] = gbitmap_create_with_resource( RESOURCE_ID_BOX1 );
...
s_boxes_bitmap[10] = gbitmap_create_with_resource( RESOURCE_ID_BOX11 );
8. Когда я впервые после долгих лет увидела эту картинку:
я подумала: вот умели же раньше делать! Вот здесь, если присмотреться, фон циклический, вот здесь циклически, и здесь… Экономили память, как могли! Вот еще статья о том, как экономили память на одинаковых облаках и кустах.
На самом деле, когда я распаковала ресурсы, то увидела, что весь фон сделан одной картинкой. Сначала я сделала так же – и сразу потеряла около 2Кб оперативной памяти там, где можно было бы обойтись вчетверо меньшим объемом.
Итак, сам совет: используйте изображения как можно меньшего размера, ибо каждое из них «висит» в оперативной памяти. Прорисуйте программно все, что только можно, к счастью, процессорной мощности хватает на 60 кадров в секунду.
Я разделила фон на части, которые циклически повторяются. Pebble автоматически повторяет изображение, если прямоугольник, в котором оно должно отображаться, больше, чем само изображение, и это сто́ит использовать. Но если переборщить и рисовать изображение размером 1x1 на весь экран, fps будет очень низким. Для таких целей лучше использовать графические примитивы – линии, прямоугольники.
9. Делайте ресурсы такими, чтобы их можно было использовать «из коробки», то есть без написания дополнительного кода.
В игре персонаж может ходить налево и направо, изображения при этом симметричные. Сначала я думала сэкономить место, и написала код, который отображает изображения зеркально. Но после того, как памяти перестало хватать, от этого кода пришлось отказаться.
10. Избегайте «долгоживущих» ресурсов там, где это не оправдано. Если после выбора режима игры меню больше не будет использоваться, уничтожайте его сразу перед игрой. Если будет использоваться – запомните его состояние и воспроизведите тогда, когда это будет необходимо. Если картинка отображается только при старте, удаляйте ее сразу после показа.
11. Используйте static-методы и static-переменные, используйте const везде, где переменную не предвидится менять.
static const char caps[] = "ABCDEFGHIJKLM NOPQRSTUVWXYZ";
лучше, чем просто
char caps[] = "ABCDEFGHIJKLM NOPQRSTUVWXYZ";
12. Используйте один и тот же callback там, где это возможно. Например, если в двух меню menu_draw_header_callback пустой, нет смысла писать его дважды.
static void menu_draw_header_callback(GContext* ctx, const Layer *cell_layer, uint16_t section_index, void *data)
{
}
menu_layer_set_callbacks(menu_layer, NULL, (MenuLayerCallbacks) {
.get_num_rows = menu_get_num_rows_callback,
.draw_header = menu_draw_header_callback,
.draw_row = menu_draw_row_callback,
.select_click = select_callback,
});
13. Используйте user_data тех объектов, которые его имеют. Память уже выделена, почему бы не использовать ее в своих целях?
14. Используйте int как основной тип, даже если следует пересчитать от 0 до 5. Думаю, выигрыш связан с тем, что компилятор вставляет дополнительный код, если используются меньшие типы.
15. Старайтесь повторно использовать код максимальное количество раз.
Этот совет похож на совет №12, но более общий. Не используйте метод копипаста с изменением нескольких строк кода после этого, вместо этого используйте флаг, который бы передавался в функцию.
16. Последний совет опаснее всех предыдущих. Сразу предупрежу, что я им не пользовалась, и никому пользоваться не рекомендую. Впрочем, бывают ситуации, когда другого выхода нет. Для того, чтобы его случайно не прочли дети за вашей спиной, прячу совет под спойлер.
int _rand(void) /* RAND_MAX assumed to be 32767. */
{
static unsigned long next = 1;
next = next * 1103515245 + 12345;
return next >> 16;
}
Результат
Игра доступна в pebble appstore, код доступен на github. Вот видео того, что получилось:
Автор: svitlana_tsymbaliuk