Однажды меня попросили разобраться с багом: при смене frameRate в произвольном количестве вложенных .swf начинал странно вести себя самописный «твинер» — класс, который интерполирует некоторое значение на заданное время. Вместо своей нормальной деятельности, твинер мог перескакивать значения, мог залипать на каком-то одном, а иногда просто в произвольный момент времени задавать переменной её конечное значение и отчитываться о завершении своей работы. Просящий связывал проблему именно с многоуровневой вложенностью и несовпадении собственного и родительского fps.
Я попытался написать код твинера с нуля и оказалось, что мой вариант тоже ведёт себя странно, несмотря на то, что уровень был всего один и fps был постоянен. В процессе решения задачи я узнал пару замечательных трюков flash, коими и тороплюсь поделиться.
Казалось бы, какие тут могут быть сложности? В конструктор собственного твинера передаём ссылку на объект, параметр, который надо бы менять, конечное значение и время/кадры, за которое этот параметр должен плавно принять конечный вид. Для простоты возьмём случай покадрового изменения значения:
Да, самым простым способом определить, когда настало время обновить значение, будет подписка на событие Event.ENTER_FRAME у передаваемого в конструктор DisplayObject. Однако мы не ищем лёгких путей и делаем твинер универсальным. То есть таким, который меняет параметры не только DisplayObject'а. Поэтому придётся использовать ещё однунедокументированную малоизвестную особенность as3:
Меняем конструктор:
Можете проверить сами. Метод tween будет исправно вызываться с частотой fps.
Идём дальше. Опять же по наипростейшему пути — каждый вызов tween мы меняем переданное значение на одну и ту же величину (твининг типа easeNone — то есть равномерный). Для этого лучше ещё в конструкторе рассчитать покадровый инкремент, исходя из разницы между конечным и стартовым значениями и продолжительностью твина, и записать инкремент в поле класса. В самом методе tween мы будем проверять, сколько кадров твининг уже длится и по достижении заданного значения — прерывать твининг:
Нет ничего страшного в том, чтобы использовать frames как счётчик оставшихся «тиков» твининга: мы договорились не использовать секунды в качестве времени твина. Более того, кроме как в конструкторе параметр frames нигде не используется и никто (даже мы) не станет нас обвинять в том, что мы меняем переданную по значению переменную в своих целях.
Казалось бы — всё, твинер готов. И он даже будет работать, а в большинстве случаев — ещё и правильно. Но есть один случай, когда он ни в коем случае не выдаст нормального результата. Приведу небольшой пример:
Что нам даст трейс в результате вызова этого кода? По логике вещей — столбец из сорока чисел уменьшающихся с шагом в 0.05. На практике же у любого DisplayObject есть ещё как минимум одна недокументированная особенность: его координаты (а возможно и некоторые другие свойства) всегда кратны 0.05. Попытка присвоить им некратное значение провалится: при следующей отрисовке оно будет округлено до ближайшего к нулю кратного. В данном конкретном примере этот эффект не должен нам угрожать (на самом деле — он проявляется во всей красе), но, к примеру, увеличив значение кадров, в течение которых должен проявиться твининг, до 80, мы получим инкремент равный 0.025 и трейс «зависнет» на нуле, так никогда и не достигнув -1.
Есть и другая особенность. В среде исполнения FlashPlayer тип Number является 64х битным числом с плавающей запятой. Из-за этого достаточно часто случаются накладки. Проще всего объяснить на примере:
Так ведёт себя флеш со всеми значениями типа Number (к коим относятся и поля координат DisplayObject), тут уж ничего не поделаешь. Вполне естественно, что пр��мер работы метода traceSome нашего класса SomeClass будет сбоить даже при твининге на 40 кадров. Практика показала, что sprite.x не сможет сдвинуться именно со значения -0.35 будучи каждый кадр до него округлённым. Цикл таков:
Избавиться от скрытого четвёртого пункта невозможно. Однако ошибка, вызванная симбиозом математики Number и ограничениями значений координат DisplayObject обходится нами ровно тремя лишними строками. Для этого необходимо изменить первый и пятый пункт цикла и вместо перманентно «портящегося» хранилища значения, завести своё:
Казалось бы, при чём тут MVC? :-)
P.S.: Да, существующий код твинера можно и нужно улучшать. Добавить отложенный запуск, рассылку сообщений. Можно добавить easing и прочий мультифилд-твин. Но к данной задаче это не относится, а потому оставим в качестве домашнего задания.
Я попытался написать код твинера с нуля и оказалось, что мой вариант тоже ведёт себя странно, несмотря на то, что уровень был всего один и fps был постоянен. В процессе решения задачи я узнал пару замечательных трюков flash, коими и тороплюсь поделиться.
Казалось бы, какие тут могут быть сложности? В конструктор собственного твинера передаём ссылку на объект, параметр, который надо бы менять, конечное значение и время/кадры, за которое этот параметр должен плавно принять конечный вид. Для простоты возьмём случай покадрового изменения значения:
public class SomeTweener {
private var _obj:Object;
private var _paramName:String;
private var _endValue:Number;
private var _frames:Number;
public function SomeTweener(obj:Object, paramName:String, endValue:Number, frames:Number) {
_obj = obj;
_paramName = paramName;
_endValue = endValue;
_frames = frames;
}
}Да, самым простым способом определить, когда настало время обновить значение, будет подписка на событие Event.ENTER_FRAME у передаваемого в конструктор DisplayObject. Однако мы не ищем лёгких путей и делаем твинер универсальным. То есть таким, который меняет параметры не только DisplayObject'а. Поэтому придётся использовать ещё одну
Любой DisplayObject, даже не добавленный в DisplayList исправно получает событие входа на кадр.
Меняем конструктор:
import flash.display.Shape;
import flash.events.Event;
import flash.events.IEventDispatcher;
public class SomeTweener {
private var _obj:Object;
private var _paramName:String;
private var _endValue:Number;
private var _frames:Number;
private var eventDispatcher:IEventDispatcher;
public function SomeTweener(obj:Object, paramName:String, endValue:Number, frames:Number) {
_obj = obj;
_paramName = paramName;
_endValue = endValue;
_frames = frames;
eventDispatcher = new Shape();
eventDispatcher.addEventListener(Event.ENTER_FRAME, tween);
}
private function tween(e:Event):void {
}
}Можете проверить сами. Метод tween будет исправно вызываться с частотой fps.
Идём дальше. Опять же по наипростейшему пути — каждый вызов tween мы меняем переданное значение на одну и ту же величину (твининг типа easeNone — то есть равномерный). Для этого лучше ещё в конструкторе рассчитать покадровый инкремент, исходя из разницы между конечным и стартовым значениями и продолжительностью твина, и записать инкремент в поле класса. В самом методе tween мы будем проверять, сколько кадров твининг уже длится и по достижении заданного значения — прерывать твининг:
import flash.display.Shape;
import flash.events.Event;
import flash.events.IEventDispatcher;
public class SomeTweener {
private var _obj:Object;
private var _paramName:String;
private var _endValue:Number;
private var _frames:Number;
private var eventDispatcher:IEventDispatcher;
private var increment:Number;
public function SomeTweener(obj:Object, paramName:String, endValue:Number, frames:Number) {
_obj = obj;
_paramName = paramName;
_endValue = endValue;
_frames = frames;
increment = (endValue - Number(obj[paramName])/frames;
eventDispatcher = new Shape();
eventDispatcher.addEventListener(Event.ENTER_FRAME, tween);
}
private function tween(e:Event):void {
if (_frames == 0) {
e.currentTarget.removeEventListener(e.type, tween);
return;
}
obj[paramName] += increment;
_frames--;
}
}Нет ничего страшного в том, чтобы использовать frames как счётчик оставшихся «тиков» твининга: мы договорились не использовать секунды в качестве времени твина. Более того, кроме как в конструкторе параметр frames нигде не используется и никто (даже мы) не станет нас обвинять в том, что мы меняем переданную по значению переменную в своих целях.
Казалось бы — всё, твинер готов. И он даже будет работать, а в большинстве случаев — ещё и правильно. Но есть один случай, когда он ни в коем случае не выдаст нормального результата. Приведу небольшой пример:
import flash.display.Sprite;
import flash.events.Event;
public class SomeClass extends Sprite {
private var sprite:Sprite;
public function SomeClass() {
sprite = new Sprite();
sprite.x = 1;
sprite.addEventListener(Event.ENTER_FRAME, traceSome);
var tween:SomeTweener = new SomeTweener(sprite, 'x', -1, 40);
}
private function traceSome(e:Event):void {
trace(sprite.x);
}
}Что нам даст трейс в результате вызова этого кода? По логике вещей — столбец из сорока чисел уменьшающихся с шагом в 0.05. На практике же у любого DisplayObject есть ещё как минимум одна недокументированная особенность: его координаты (а возможно и некоторые другие свойства) всегда кратны 0.05. Попытка присвоить им некратное значение провалится: при следующей отрисовке оно будет округлено до ближайшего к нулю кратного. В данном конкретном примере этот эффект не должен нам угрожать (на самом деле — он проявляется во всей красе), но, к примеру, увеличив значение кадров, в течение которых должен проявиться твининг, до 80, мы получим инкремент равный 0.025 и трейс «зависнет» на нуле, так никогда и не достигнув -1.
Есть и другая особенность. В среде исполнения FlashPlayer тип Number является 64х битным числом с плавающей запятой. Из-за этого достаточно часто случаются накладки. Проще всего объяснить на примере:
trace(String(-.35 - .05)) // 0.39(9)97Так ведёт себя флеш со всеми значениями типа Number (к коим относятся и поля координат DisplayObject), тут уж ничего не поделаешь. Вполне естественно, что пр��мер работы метода traceSome нашего класса SomeClass будет сбоить даже при твининге на 40 кадров. Практика показала, что sprite.x не сможет сдвинуться именно со значения -0.35 будучи каждый кадр до него округлённым. Цикл таков:
- Берём значение поля объекта (-0.35)
- Наращиваем его на значение инкремента (-0.35 + (-0.05) = -0.39(9)97)
- Записываем его в поле объекта (-0.39(9)97)
- (Скрытый обязательный пункт) Значение поля координаты экземпляра DisplayObject округляется (-0.35)
- Входим на следующий кадр и берём значение поля объекта (-0.35)
- GOTO 2
Избавиться от скрытого четвёртого пункта невозможно. Однако ошибка, вызванная симбиозом математики Number и ограничениями значений координат DisplayObject обходится нами ровно тремя лишними строками. Для этого необходимо изменить первый и пятый пункт цикла и вместо перманентно «портящегося» хранилища значения, завести своё:
import flash.display.Shape;
import flash.events.Event;
import flash.events.IEventDispatcher;
public class SomeTweener {
private var _obj:Object;
private var _paramName:String;
private var _endValue:Number;
private var _frames:Number;
private var eventDispatcher:IEventDispatcher;
private var increment:Number;
private var currentValue:Number;
public function SomeTweener(obj:Object, paramName:String, endValue:Number, frames:Number) {
_obj = obj;
_paramName = paramName;
_endValue = endValue;
_frames = frames;
currentValue = Number(obj[paramName]);
increment = (endValue - Number(obj[paramName])/frames;
eventDispatcher = new Shape();
eventDispatcher.addEventListener(Event.ENTER_FRAME, tween);
}
private function tween(e:Event):void {
if (_frames == 0) {
e.currentTarget.removeEventListener(e.type, tween);
return;
}
currentValue += increment;
obj[paramName] = currentValue;
_frames--;
}
}Казалось бы, при чём тут MVC? :-)
P.S.: Да, существующий код твинера можно и нужно улучшать. Добавить отложенный запуск, рассылку сообщений. Можно добавить easing и прочий мультифилд-твин. Но к данной задаче это не относится, а потому оставим в качестве домашнего задания.
