Показаны сообщения с ярлыком Нюансы. Показать все сообщения
Показаны сообщения с ярлыком Нюансы. Показать все сообщения

суббота, 17 декабря 2011 г.

Отзывчивый интерфейс на Android

Если вы делате что-то сверх тривиального Hello World, то обеспечить "моментальную", без всяких тормозов реакцию интерфейса на действия пользователя дело не такое тривиальное, как это может показаться. Рассмотрим это на примере из простого графического редактора, который я сейчас разрабатываю.

 

Встроенного механизма для хранения всех операций рисования нет, при каждом вызове процедуры рисования мы должны нарисовать всю картинку. Первый, логичный шаг - сделать класс DrawOperation, хранить все экземпляры в упорядоченном списке и в процедуре рисования быстренько по этому списку пробежаться и все нарисовать. При этом заодно элементарно решается выполнение undo: удалили последний элемент из списка и привет. В каждом экземпляре DrawOperation хранится траектория движения "кисти", вид кисти, цвет, размер и т.п. Очень удобно и логично.

Проблема лишь в том, что "быстренько" не выйдет. Уже после нескольких мазков нашей виртуальной кистью отрисовка начинает заметно притормаживать.

Шаг второй, не менее логичный. Заводим виртуальный холст (Bitmap+Canvas), на котором и будем выполнять само рисование, а затем просто выводить его поверх заранее подготовленного фона. А список операций будем вести параллельно, он нам таки пригодится для undo.

Замечу (но разъяснять не стану, поверьте  - он понадобится), что в действительности придется завести еще один виртуальный холст, для текущей операции. По завершении они объединяются, а в процессе накладываются по очереди.

Все замечательно, но все равно притормаживает, а undo выполняется вообще неимоверно долго.

Шаг третий, вместо списка операций храним список "снимков" экрана. Сжатие в PNG позволяет нам хранить довольно большое их количество, делаем это во вспомогательном потоке с меньшим приоритетом, запускаем его когда текущая операция завершается и возникает естественная пауза, пока пользователь не ведет стилусом (пальцем, языком или чем там еще) по экрану, а переставляет его на новое место.

Undo вписалось в приемлемые рамки, теперь мы просто достаем предпоследний снимок, а последний удаляем. Ну а чего же оно тормозит-то ТЕПЕРЬ?

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

Шаг четвертый. Делаем статический класс Trash, который будет у нас содержать коллекцию объектов, которые надо бы удалить, но не хочется вызвать этим появление на сцене сборщика мусора. И конечно у него будет метод освобождающий весь этот хлам - в тот момент, когда мы надеемся на лучшее ничем важным не заняты. Это не гарантирует, что сборщик вызовется именно в этот момент, но скорей всего так и произойдет, особенно если подсказать системе вызовом System.gc()

Шаг пятый, шерстим весь код и избавляемся от лишнего создания/удаления объектов. Т.е. если объект можно использовать повторно, то лучше его повторно использовать. В частности не зачем для каждой операции создавать новый Bitmap+Canvas, лучше просто почистить тот, что остался от предыдущей.

 

И конечно лучше об этом думать сразу.

среда, 7 декабря 2011 г.

Ошибка резидента MotionEvent & SurfaceView

После пары дней пыхтения и потения над отладкой тривиального графического редактора, обкусав ногти до локтей и выщипав все волосы в разных местах выяснил два момента. Может кому пригодится если сумеет найти данный текст.

Первая проблема известна в англоязычных энторнетах как SurfaceView flickering. Так вот если у вас картинка моргает, то почти сто процентов уверенности, что вы накосячили с синхронизацией потоков. Т.е. рисуете вы в одном потоке, а готовите картинку в другом и где-то лопухнулись. Вдумчиво изучите как у вас происходит синхронизация, прокол может быть совершенно неочевидным. Второй вариант - вынести все действия по подготовке картинки в один поток, тот же, что занимается рисованием. Тут нас может подстерегать другая проблема, названия ей я не нашел, но крови она мне попортила изрядно.

Итак, есть у нас обработчик кликов:

boolean onTouchEvent(MotionEvent e)

и есть желание эту обработку перенести в тот же поток, где у нас происходит формирование картинки. К слову сказать, SurfaceView был разработан в частности для того, чтобы эту обработку разнести по разным потокам :), но иногда это все таки может быть нужно. Так вот, делаем мы это элементарно, вариантов много, но все они сводятся к тому чтобы поместить событие в очередь, а в другом потоке эту очередь опросить и обработать. Так вот внимание, сюрприз:

 

Объект MotionEvent e, который получает наш обработчик передавать в другой поток для отложенной обработки НЕЛЬЗЯ. Потому что он существует в единственном экземпляре и в наш обработчик передается лишь ссылка на этот экземпляр. И как только вы выйдете из обработчика (положив эту ссылку в очередь), система с чистой совестью может поместить туда новое значение. В результате, если обработка у нас запаздывает, и мы успели  положить к себе в очередь три события MotionEvent, то все три будут указывать на одно и то же событие.

В результате мы одновременно а) теряем события и б) они у нас дублируются. Я плакаль...

Хрен вы это где найдете в документации. Это наглядный пример результатов телепатической связи между разработчиками Android SDK (которые полагают подобное поведение очевидным) и конечными его пользователями.

Вывод: верить можно только себе, и то, с осторожностью. Т.е. любой системный объект (в данном случае MotionEvent) должен быть обработан там, где он получен  и не следует рассчитывать что он сохранит своё состояние после выхода из обработчика. Если нам нужно сделать "ленивую обработку", то нужно получить "твердую копию", т.е. объект состояние которого полностью контролируется нашей программой, а не системой.

понедельник, 5 декабря 2011 г.

Андроид. Вести с полей

Наконец-то я плотно занялся этим девайсом. Делаю несложный графический редактор, узнаю много нового и интересного.

 

Моя первая попытка сделать его с использованием OpenGL ES1.0 потерпела полнейшее фиаско. Для редактора пиксельной графики OpenGL (по крайней мере ES1.0) подходит как квадратные колеса для велосипеда. Т.е. как-то приспособить может быть и можно, и даже едет, но совершенно не тривиально и с жутким скрыпом.

 

Вторая попытка была более удачной. Обычный Canvas заточен как раз под пиксельную графику, без всяких заморочек с 3D. Осложняет задачу то что я с ним не работал никогда (в отличие от OpenGL) и многие проблемы и их решение для меня неочевидны. А от некоторых спецэффектов, появившихся в процессе работы у меня волосы встали дыбом во всех местах, я так и не понял как такое возможно. Скажем появился у меня объект класса Picture, который по разному рисуется при четном и нечетном вызове процедуры рисования. 

 

Пришлось сразу задуматься об оптимизации. Если рисовать всю картинку каждый раз заново - быстродействие падает стремительными темпами. Хранить все действия в Picture - возможный вариант, но не столь тривиальный, как хотелось бы. В результате мучительных поисков, решение было найдено и оказалось совершенно тривиальным, более того, о нем вскользь упомянуто в гугловской документации. Их документация это вообще отдельная тема. Полнотой и подробностью она не страдает. 

 

Если кому интересно - нужно создать Bitmap, обернуть его в Canvas и спокойно рисовать туда. А когда дело доходит до обновления экрана - рисовать этот Bitmap в Canvas экрана. В этом случае время отрисовки стабильно и достаточно невелико.

 

P.S. Всплыл интересный факт - размер сенсорной области у меня меньше видимой области экрана. Если разместить какой-то мелкий элемент на краю экрана, то для нажатия он будет недоступен. Практический пример: ползунок (SeekBar) шириной во весь экран невозможно довести до крайних положений.

среда, 23 ноября 2011 г.

Некоторые замечания

Долго колебался, делать ли значения типа (корабля, оборудования, сообщения) цифровыми или строкой. В конце концов во многих случаях склонился к строке. Пусть это медленнее, и отнимает больше памяти но зато проще делать экспорт/импорт данных в/из человекочитаемый формат. Да и при отладке проще догадаться что означает "user.login" в поле msgtype, чем 42.

 

Жалко switch (строка) появился только в Java 1.7

вторник, 22 ноября 2011 г.

IOException: Read end dead

Сегодня затеял небольшой рефакторинг и очень удивился получив вот такой вот эксепшн и еще ряд интересных глюков. Выяснил интересную вещь:

допустим у нас есть поток 1, который создает поток 2, а сам тихо завершается - за не надобностью. Если у вас возник вопрос зачем это надо, сразу могу сказать - это только тестовый пример. В действительности у меня их запускается несколько штук, а родительский можно как-то использовать, но пока это не сделано, да и стройность программы от этого пострадает. 

Так вот, родительский поток 1 среди прочего инициализирует парочку PipedInput/OutputStream, а использоваться эта связка будет исключительно в потоке 2. Тут и ожидает сюрприз. Эти Piped как-то привязаны к потоку в котором были созданы, и если он завершается, то при попытке их использовать получаем тот самый эксепшн. Открытие неприятное ввиду того что

а) совершенно не очевидно

б) непонятно, касается ли это чего-то еще? Какие еще ресурсы не являются в этом отношении thread-safe?

с) возникает необходимость как-то отслеживать жизненый цикл таких объектов в многопоточной программе. Если у вас создаются временные потоки для решения каких-то задач, и идет интенсивный обмен объектами между ними, то повышается вероятность наступить на эти грабли. Потоки не должны обмениваться объектами, связанными с системными ресурсами? Они все должны создаваться в одном "родительском" потоке?

д) непонятно, на что еще может повлиять такая зависимость? А не окажется ли, что используя некоторые объекты (те же PipedInput/OutputStream) я неявным образом могу вызывать блокировку потока 2, из-за необходимости  синхронизации его с потоком 1 (который в это время занят чем-нибудь)?

 

пятница, 11 ноября 2011 г.

Выбор объектов в OpenGL

Выбор объекта мышкой кажется в режиме 2D делом достаточно тривиальным. Вот есть список объектов, вот координаты мышиного указателя. Пробегаем по списку, сравниваем и выбираем тот объект который ближе.

 

Вот только объекты у меня ни разу не прямоугольные, не треугольные и вообще не скажешь точно какой формы. Объекты у меня формы совершенно произвольной (хоть с дырками). Поэтому проверять попадание перебором объектов в списке дело маятное, мы пойдем другим путем.

    FloatBuffer fb=FloatBuffer.allocate(1);

    gl.glReadPixels(x,h-y-1, 1, 1, GL2.GL_DEPTH_COMPONENT, GL.GL_FLOAT, fb);

получаем z координату пикселя под мышью - это z координата самого верхнего из попавших под мышь объектов. А все объекты мы предварительно разнесли по z-координате и создали карту z -> объект. Вот и все - достаем из карты объект и радуемся.

 

А вот и не все... Вокруг каждого объекта есть квадратная прозрачная область (мы берем текстуру и натягиваем ее на квадрат). И как выяснилось - прозрачная она лишь для меня, но не для glReadPixels. В результате мы попадаем в объект просто ткнув рядом. Хуже того - мы тыкаем в один объект, а попадаем в другой, который этой прозрачной областью его перекрывает. Однако решение этой проблемы тривиально до безобразия, и видимо из-за этого его хрен найдешь.

 

Ставим в начало программы:

     gl.glEnable(GL2.GL_ALPHA_TEST);

    gl.glAlphaFunc(GL2.GL_GREATER, 0.01f);

В результате пиксели со значением alpha<0.01 (а у нас ровно 0) в буфер не попадают и не портят нам малину. Теперь объект можно выбрать хоть через дырку в другом объекте.

Пришлось еще переставить обработку мышиных событий - теперь мышь обрабатывается после отрисовки.

А еще надо помнить, что машина может представить десятичное значение совсем не так как мы ожидаем (например 0.003999 вместо 0.004), так что формируя индекс нужно его аккуратненько округлить и не полагаться лишний раз на автоматическое приведение типов.

Ну и не стоит забывать что в буфере глубины у нас значение 0 : 1.0, а объекты мы рисуем в диапазоне -1.0 : 1.0, а то я все не мог понять, что за левые координаты получаются

float z=fb.get(0);

z=1 - (z*2);

З.Ы. Завтра буду учить свои кораблики стрелять

суббота, 5 ноября 2011 г.

Мышиная возня

Если мы делаем шутер, то нужно отлавливать в первую очередь события MOUSEPRESSED, а не MOUSECLICKED. Иначе, если игрок быстро щелкает мышью, то эффект возникает странный. MOUSEPRESSED генерится на каждый щелчок, а MOUSECLICKED может вообще не сгенерироваться. Хотя по идее должно - с количеством нажатий. Подозреваю что там идет проверка чтобы нажатия были рядом, и если нет - многократный щелчок просто отбрасывается.

К слову сказать, нормальных двойных щелчков я тоже не наблюдал. Если делаешь нормальный двойной щелчок - идут два щелчка.

Так что вместо MOUSECLICKED - MOUSEPRESSED, а если же нам все таки нужен двойной щелчок, то реализуем его логику сами - ловим MOUSE_PRESSED, отслеживаем координаты и время между кликами, а дальше - если то что одинарный клик уже прошел для нас несущественно, то все легко и просто, а если существенно... Нужна некоторая задержка в обработке кликов. Проще всего сделать подсчет времени при рендеринге (все равно обработка мыши у нас там) и брать в дальнейшую обработку клики которые отлежались заданное время, в течение которого они могут превратиться в двойные клики... Уфф...

 

Обработка мышиных событий при рендеринге

 Как меня угораздило? А жизнь заставила. Собственно сейчас (при работе в 2D) в этом большой необходимости нет, но этот кусок унаследован от предыдущего (3D) варианта... А там чтобы выяснить куда мы мышью попали, нужно было произвести некий танец с бубном вокруг gluUnProject. И танец этот нужно проводить там, где мы имеем доступ к текущим матрицам - уже полностью подготовленнным. Что возможно только при рендеринге.

Более того, если мы по событиям мыши передвигаем камеру и т.д. и т.п., то матрицы меняются и картинку желательно тут же перерисовать. Так что алгоритм выглядит примерно так

  1. Формируем матрицы
  2. Проверяем очередь событий мыши (а в стандартном обработчике мы их туда заталкиваем)
  3. Обрабатываем мышь, если есть что обрабатывать
  4. Если надо заново формируем матрицы
  5. теперь только рисуем

В 2D координаты отслеживаются тривиально, можно обойтись без этого, ну а если вдруг потом приспичит перейти на 3D?

 

P.S. Как же иногда задалбывают слишком "умные" программы. Ну зачем, скажите, ScribeFire упорно заменяет символ _ на выделение курсивом? Причем не везде, а выборочно? 

P.P.S. Ну где бы найти хороший блог-клиент, чтобы все было удобно, без глюков и работало под Ubuntu?

пятница, 4 ноября 2011 г.

Оптинческий обман

Обнаружил интересный феномен. 

 

Если нарисовать медленно движущуюся точку (смещать ее совсем понемногу, со скоростью пиксель в секунду или меньше) - глаз воспринимает это как дерганье. Раз в секунду точка рывком смещается на пиксель.

 

А если точка движется быстро - на десятки пикселей в секунду, то движение воспринимается как плавное.

 

Отсюда интересный вывод: всякие фоновые движения (типа летящие метеоры - практически непременный атрибут космических симуляторов ) должны происходить в определенном диапазоне скоростей, иначе это выглядит откровенно убого.

P.S. Вести с полей:

  • Максимум что может выжать моя прога на полном экране 1440х900 - 45 FPS. Это при том что она практически ничего не рисует (время рендеринга <1мс).
  • сделал пресловутые метеоры