Эта статья посвящена оптимизации ActionScript3 на примере создания эффекта «полета сквозь звезды»(на подобии старого скринсейвера Windows). Мне этот эффект необходим был для игры. Эффект должен был служить фоном и поэтому требовалось чтобы он был как можно менее ресурсоемким.
Я не изобретаю никакие новые приемы оптимизации, я лишь приведу пример их применения на практике. Для начала я поискал в интернете готовые решения и наткнулся на решение, которое визуально меня устраивало: . Открыв код мы обнаружим, что эти Звездочки — это MovieClip'ы (крик ужаса за кадром). Каждый флеш-разработчик понимает, что кроме отображения «точки» на каждую «Звездочку-MovieClip» вешается куча свойств и методов класса MovieClip. И чем больше звезд — тем хуже. Я ничего не хочу сказать об авторе — просто нам требуется другое.
Ясно, что в такой ситуации прийдется сделать все самому и с нуля. Ничего гениального в нашей реализации нет:
создать массив точек stars:Array
в каждом кадре:
… просчитать новое положение каждой точке по суперсложной математической формуле (умножение).
… нарисовать каждую точку на экране.
Сразу оговорюсь, я использую собственный метод получения случайного значения в классе :
public static function rand(lowValue:Number, hightValue:Number, round:Number = 1):Number{
return lowValue + Math.floor(Math.random() * (hightValue-lowValue)*round)/round;
}
Реализуем наш нехитрый алгоритм и получаем класс . Код нам не важен. Но именно такой код я написал бы до того, как начал закомиться с методами оптимизации когда.
Звезд у нас будет меньше чем на небе, создадим их всего 2000 (реально их хватит 100-200, но нам для теста нужно побольше):
background = new StarFieldBad(_game.STAGE.stageWidth, _game.STAGE.stageHeight, 2000);
addChild(background);
Выполнение инициализации не является критическим параметром, т.к. создание объекта делается не часто. Но все же среднее время создания:7,42ms.
Критическим показателем в нашем случае является время выполнения кадра: 9,1095ms.
Вроде бы и подход не самый страшный, и звезды не создаются каждый раз, но на самом деле это все не верх совершенства. Итак, пойдем по порядку.
bitmapData.lock()
В коде не хватает использования метода lock() и unlock(). Добавляем их. (Подробнее «» на сайте Adobe.)
В итоге время выполнения стало: 8,9753ms. (Печально, маленький прирост)
Цикл FOR
...for (var i:int = 0; i < stars.length; i++) // Проходимся по всем звездам...
Цикл при каждом проходе обращается к объекту и его свойству. Заодно заменяем цикл for на цикл while:
var i:uint = starsCount;// Итератор звезд
while ( --i > -1)// Проходимся по всем звездам
...
Время выполнения: 7,5379ms. (Удивительно большой прирост)
По возможности избегайте использования оператора квадратной скобки
Например у нас внутри циклов происходят подобные операции:
Заменяем оператор квадратные скобки на ссылку в локальной переменной (имеет смысл, если мы обращаемся к переменной много раз):
var star:Point;// Ссылка на мат. модель звезды
while ( --i > -1)// Проходимся по всем звездам
{
star = stars[i];
star.x += (star.x - halfWidth) * 0.01;
...
Время выполнения: 4.4279ms. (Невероятно большой прирост. Видимо, ещё сказывается большой объем массива)
По возможности используйте класс Vector вместо класса Array
Заменяем Array на Vector.<Point>. «В проигрыватель Flash Player 10 добавлен класс Vector, который обеспечивает более быстрый доступ для чтения и записи, чем класс Array.» ()
stars = new Vector.<Point>(starsCount, true);
Время выполнения: 3.0963ms.
Избавляем Flash от ненужных повторяющихся вычислений
Можно заранее просчтитать значения таких выражений как «fieldWidth / 2» на «halfWidth» или «fieldHeight+offset» на «fieldWidthWithOffset» и сохранить их в переменные, тем самым избавив флеш от ~20000 лишних операций деления и сложения(это в нашем примере) за кадр:
Время выполнения: 2.9642ms. (ну, большого прироста никто и не обещал)
Другие приемы оптимизации
Далее я опущу описание других методов, которые мало сказались на производительности (но это не значит, что нужно ими пренебрегать!):
Определение переменных вне цикла.
Встраивание кода для уменьшения числа вызовов функций в коде.
В итоге время выполнения скрипта было уменьшено с 9,7753ms до 2.9405ms. А это более чем в 3 раза! И время инициализации сократилось в 2 раза — с 7,42ms до 3,26ms.
Файл .
Исходный файл .
Итоговый файл
Эти и подобные методы оптимизации следует научится использовать сразу, чтобы в последующем не пришлось проводить оптимизацию кода. Всем творческих успехов!
Да, лучше сразу учится писать оптимальный код, потому что дополнительная оптимизация потом потребует не только силы на переработку алгоритмов, но и время на их тестирование :(
Не согласен. Если подобные правила как в этом посте брать себе за стандарт (вызубрить и не забывать), то и скорость разработки будет нормальная и код будет «быстрым».
Если математическая операция выполняется 2 раза, стоит ли кешировать ее результат? А 10 раз? А 5000 раз? С какого количества выполнения нужно оптимизировать?
Два раза каждый кадр или два раза вообще за всю работу приложения? :) Я думаю, что у хорошего программиста таких вопросов не должно возникать, он должен делать все на автомате и не копаться потом в коде выискивая медленные операции ;)
Я более чем согласен. На самом деле, когда я писал этот класс, сначала был написал «правильный» оптимизированный класс. Потом я решил написать статью и сделал класс «как быть не должно». Написание оптимизированного кода заняло столько же времени, как и написание обычного.
+1
Не так сложно и напряжно следовать хотя бы основным правилам, чтобы потом не выискивать и переписывать все:
— не использовать в циклах stars.length (каждый раз высчитывается)
— если к элементу массива обращаются много раз, сохранить его в локальную переменную
— Vector быстрее Array
А потом если нужно дальше, то уже можно изгаляться — умножение быстрее деления, битовые операции быстрее умножения и деления.
Вынести часто вычисляемые значения в переменные (константы)
+1, подписываюсь как программист с многолетним стажем и опытом работы вплоть до лид-программера в игровых фирмах.
Часто бывает, что оптимизация под конец хорошо проходит, поскольку профайлер сразу показывает ботлнек и ты его оптимизируешь. Но некоторые массовые вещи типа «если к элементу массива обращаются много раз, сохранить его в локальную переменную» не делать — боттленеков может и не быть. И оптимизировать уже не получится малой кровью.
За всю жизнь столкнулся с необходимостью оптимизации кода только 2 раза, для iPhone игры и для j2me игры, и то по причине ограничений платформы. Преждевременная оптимизация — зло, т.к. порождает нечитабельный, плохо поддающийся изменению код. Ума не приложу, как можно написать тормозящую по причине кода на флеш игрушку. Т.е. сложная анимация или слишком сложная физическая сцена — да, но чтобы не оптимизированные операторы на что-то влияли — очень сомнительно.
Ну, я про преждевременную оптимизацию говорил. В случае с боксом требуется оптимизация уже написанного и отлаженного кода. Ну и все-таки бокс — это библиотечка, а не игра :).
Время инициализации проверялось путем создания 100 раз объекта и замера времени выполнения функций в конструкторе c помощью getTimer();
Время выполнения считалось тоже через getTimer() за 2000 циклов и находилось среднее значение. Т.е. я дополнительно вписывал код в программу. На самом деле код при тестировании выглядел так:
В дебажном плеере(делаю вывод из того, что используется trace) измерять скорость выполнения — не корректно, в релизном плеере скорости выполнения различных команд может отличаться, как в положительную так и отрицательную сторону.
Просто — как мысль. Есть еще один очень неплохой способ оптимизации кода, который, как мне кажется подходит к этой ситуации.
Идея в том — что бы по другому организовать цикл. Как понял — в примере выше сделано так — что на каждом цикле каждый объект перемещается. (очень внимательно в код не вчитывался — так что мог и ошибиться)
То есть у него
— апейтится его позиция (а насколько мне известно — апдейт позиции даже с целочисленными координатами приводит к перерендеру)
— так же на каждом цикле происходит перерасчет значений.
То есть — на каждом цикле идет куча операций. Но — если переписать код таким образом — что бы перерисовывать точки и, как следствие — рендерить их только тогда, когда реально точка поменялась. Тогда — все будет намного лучше.
Грубо — только в виде примера — оно будет выглядеть так
— Создаем массив точек. У каждой точки есть специальная величина. Время, через которую точка сместиться на 1 пиксель (то есть время, через которое нужно эту точеу апдейтить). назовем ее — ulDeltaT. Чем быстрее точка двигается — тем эта величина будет меньше. (то есть — если точка двигается по одной оси — ulDeltaT = 1 / fMotionSpeed)
Так же у точки есть время, в которое ее нужно будет двигать — ulNextT.
Логика работы такая вот.
— Весь массив сортируем по ulNextT.
Хотя, вообще говоря — точки лучше хранить не в виде массив, а в виде связанного списка — тогда с ними проще будет работать (быстрее будут операции добавления\убирания точки)
— Таким оразом — получаем массив, в котором очень легко определить — какие точки нужно двигать и когда. Простым просмотром массива точек от его «головы».
В цикле игры — инкрементим базовый счетчик. И на каждом игровом цикле — берем те точки, для которых подошло время. И двигаем их.
Точку, после того, как ее заапдейтили — меняем ее ulNextTime — и вставляем на новое место в массиве.
Таких точек будет не много, и потому количество расчетов и рендера — так же будет немного. Приросты по скорости получаются — по моему опыту — очень и очень большие. На порядки больше, чем от любых других способов.
В примере выше — логика очень простая. Но — вообще говоря — такой «событийный» подход к организации цикла — можно использовать и в намного более сложном игровом цикле.
Взял число с 7ю знаками после запятой и округлял.
При числе итераций в 10'000'000 вот средние (тест повторялся 20 раз) значения:
Math.floor — 1066.76 ms
int — 78.80 ms
>>0 — 80.52 ms
Видно, что Math.floor явно проиграл. На счёт остальных двух: результаты варьируются (то один больше другого, то наоборот), но разница на столько не велика, что можно её не учитывать. :)
Автор блога по последним ссылкам, похоже, реально маньяк)) Днем ковыряет байткод, а ночью с резиновым фаллосом бегает за старушками в парке, не иначе как)))
Еще малоизвестная фишка, по слухам: (для целых, а может и не только) x = a + b + c; работает в 3 раза медленнее, чем x = a + b; x += c;
Так зайдите на форум, он специально для этого. Зачем делать исключения, если можно зашариться на форум (раз время и желание есть пообсуждать что-то эдакое...}
ну хоть что-нибудь расскажи, реально интересно же, и я уверен не только мне :)
А автору цитаты анреспет — какая разница сколько игр сделано, главное чтобы...}
ну обман скорее психологический :) скачивая Angry Birds за бесплатно — ты ожидаешь полную версию Angry Birds (просто тебе повезло и игра сегодня бесплатная :) такое ...}
На 50 коментов ни одного с критикой.
Генератор уровней по-моему сыроват, из-за этого игра местами хардкорная:
Трамплин иногда запускает игрока прямиком ...}
Нет возможности читать вам лекции, тематических книг очень много. Вкратце.
Предсказания будущего — это громко сказано. На 90% (допустим) все события в бу...}
Комментарии (38)
RSS свернуть / развернутьAntKarlov
Rigo
FlashRush
Rigo
OlegAntipov
AntKarlov
Rigo
AntKarlov
Regul
Не так сложно и напряжно следовать хотя бы основным правилам, чтобы потом не выискивать и переписывать все:
— не использовать в циклах stars.length (каждый раз высчитывается)
— если к элементу массива обращаются много раз, сохранить его в локальную переменную
— Vector быстрее Array
А потом если нужно дальше, то уже можно изгаляться — умножение быстрее деления, битовые операции быстрее умножения и деления.
Вынести часто вычисляемые значения в переменные (константы)
abyss
Часто бывает, что оптимизация под конец хорошо проходит, поскольку профайлер сразу показывает ботлнек и ты его оптимизируешь. Но некоторые массовые вещи типа «если к элементу массива обращаются много раз, сохранить его в локальную переменную» не делать — боттленеков может и не быть. И оптимизировать уже не получится малой кровью.
scmorr
Smrdis
scmorr
Smrdis
scmorr
Smrdis
Prizrak
Время выполнения считалось тоже через getTimer() за 2000 циклов и находилось среднее значение. Т.е. я дополнительно вписывал код в программу. На самом деле код при тестировании выглядел так:
Regul
Prizrak
Claymore
Claymore
Regul
Claymore
Regul
сравните ваш вариант с while с этим:
qdrj
Smrdis
elmortem
Идея в том — что бы по другому организовать цикл. Как понял — в примере выше сделано так — что на каждом цикле каждый объект перемещается. (очень внимательно в код не вчитывался — так что мог и ошибиться)
То есть у него
— апейтится его позиция (а насколько мне известно — апдейт позиции даже с целочисленными координатами приводит к перерендеру)
— так же на каждом цикле происходит перерасчет значений.
То есть — на каждом цикле идет куча операций. Но — если переписать код таким образом — что бы перерисовывать точки и, как следствие — рендерить их только тогда, когда реально точка поменялась. Тогда — все будет намного лучше.
Грубо — только в виде примера — оно будет выглядеть так
— Создаем массив точек. У каждой точки есть специальная величина. Время, через которую точка сместиться на 1 пиксель (то есть время, через которое нужно эту точеу апдейтить). назовем ее — ulDeltaT. Чем быстрее точка двигается — тем эта величина будет меньше. (то есть — если точка двигается по одной оси — ulDeltaT = 1 / fMotionSpeed)
Так же у точки есть время, в которое ее нужно будет двигать — ulNextT.
Логика работы такая вот.
— Весь массив сортируем по ulNextT.
Хотя, вообще говоря — точки лучше хранить не в виде массив, а в виде связанного списка — тогда с ними проще будет работать (быстрее будут операции добавления\убирания точки)
— Таким оразом — получаем массив, в котором очень легко определить — какие точки нужно двигать и когда. Простым просмотром массива точек от его «головы».
В цикле игры — инкрементим базовый счетчик. И на каждом игровом цикле — берем те точки, для которых подошло время. И двигаем их.
Точку, после того, как ее заапдейтили — меняем ее ulNextTime — и вставляем на новое место в массиве.
Таких точек будет не много, и потому количество расчетов и рендера — так же будет немного. Приросты по скорости получаются — по моему опыту — очень и очень большие. На порядки больше, чем от любых других способов.
В примере выше — логика очень простая. Но — вообще говоря — такой «событийный» подход к организации цикла — можно использовать и в намного более сложном игровом цикле.
uofk
MidnightOne
порокаоптимизаторства, по слухам num>>0 быстрее int(num) ;)Claymore
Провёл тест:
Взял число с 7ю знаками после запятой и округлял.
При числе итераций в 10'000'000 вот средние (тест повторялся 20 раз) значения:
Math.floor — 1066.76 ms
int — 78.80 ms
>>0 — 80.52 ms
Видно, что Math.floor явно проиграл. На счёт остальных двух: результаты варьируются (то один больше другого, то наоборот), но разница на столько не велика, что можно её не учитывать. :)
MidnightOne
Regul
Тут можно поглядеть наглядно все (слева снизу меняются страницы)
abyss
MidnightOne
PS:
Нашёл там в комментариях:
Ceil(x) == int(x + 0.9999) //Вместо int(x) + 1
И после этого я наконец понял как оптимизировать округление:
round(x) == int(x + 0.50001)
MidnightOne
round(x) = int(x + 0.5)
abyss
Еще малоизвестная фишка, по слухам: (для целых, а может и не только) x = a + b + c; работает в 3 раза медленнее, чем x = a + b; x += c;
Claymore
abyss
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.