Форум Flasher.ru
Ближайшие курсы в Школе RealTime
Список интенсивных курсов: [см.]  
  
Специальные предложения: [см.]  
  
 
Регистрация Блоги Правила Справка Пользователи Календарь Поиск рулит! Сообщения за день Все разделы прочитаны
 

Вернуться   Форум Flasher.ru > Блоги > BuKT

Оценить эту запись

Работа с математикой Number при твининге DisplayObject

Запись от BuKT размещена 06.03.2012 в 08:57
Обновил(-а) BuKT 07.03.2012 в 00:03

Казалось бы, какие тут могут быть сложности? В конструктор собственного твинера передаём ссылку на объект, параметр, который надо бы менять, конечное значение и время/кадры, за которое этот параметр должен плавно принять конечный вид. Для простоты возьмём случай покадрового изменения значения:
Код AS3:
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'а. Поэтому придётся использовать ещё одну недокументированную малоизвестную особенность as3:
Цитата:
Любой DisplayObject, даже не добавленный в DisplayList исправно получает событие входа на кадр.
Меняем конструктор:
Код AS3:
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 мы будем проверять, сколько кадров твининг уже длится и по достижении заданного значения - прерывать твининг:

Код AS3:
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 нигде не используется и никто (даже мы) не станет нас обвинять в том, что мы меняем переданную по значению переменную в своих целях.

Казалось бы - всё, твинер готов. И он даже будет работать, а в большинстве случаев - ещё и правильно. Но есть один случай, когда он ни в коем случае не выдаст нормального результата. Приведу небольшой пример:
Код AS3:
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х битным числом с плавающей запятой" © i.o. Из-за этого достаточно часто случаются накладки. Проще всего объяснить на примере:
Код AS3:
trace(String(-.35 - .05)) // 0.39(9)97
Так ведёт себя флеш со всеми значениями типа Number (к коим относятся и поля координат DisplayObject), тут уж ничего не поделаешь. Вполне естественно, что пример работы метода traceSome нашего класса SomeClass будет сбоить даже при твининге на 40 кадров. Практика показала, что sprite.x не сможет сдвинуться именно со значения -0.35 будучи каждый кадр до него округлённым. Цикл таков:
Цитата:
1) Берём значение поля объекта (-0.35)
2) Наращиваем его на значение инкремента (-0.35 + (-0.05) = -0.39(9)97)
3) Записываем его в поле объекта (-0.39(9)97)
4) (Скрытый обязательный пункт) Значение поля координаты экземпляра DisplayObject округляется (-0.35)
5) Входим на следующий кадр и берём значение поля объекта (-0.35)
6) GOTO 2)
Избавиться от скрытого четвёртого пункта невозможно. Однако ошибка, вызванная симбиозом математики Number и ограничениями значений координат DisplayObject обходится нами ровно тремя лишними строками. Для этого необходимо изменить первый и пятый пункт цикла и вместо перманентно "портящегося" хранилища значения, завести своё:
Код AS3:
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 и прочий мультифилд-твин. И да, весьма полезным, хоть и расходующим память, будет подход с предварительным предрассчётом массива всех позиций параметра, подвергаемого твинингу. Но к данной задаче это не относится, а потому оставим в качестве домашнего задания.
Всего комментариев 24

Комментарии

Старый 06.03.2012 11:55 MikroAcse вне форума
MikroAcse
 
Аватар для MikroAcse
Отлично! Молодец!
Очень полезно.
Ps: дайте ссылку на формулы easing.
Старый 06.03.2012 12:13 -De- вне форума
-De-
 
Аватар для -De-
Инкремент не нужен. Easing как делать будете?
значение = Начальное * (1 - alpha) + конечное * alpha
Старый 06.03.2012 12:41 BuKT вне форума
BuKT
 
Аватар для BuKT
Цитата:
Easing как делать будете?
Меняем поле increment на свойство и в геттере рассчитываем по текущей формуле. Разумеется, нужен будет ещё один каунтер прошедшего времени для передачи аргументом в формулы изинга, но это уже мелочи :-)
Старый 06.03.2012 12:42 ChuwY вне форума
ChuwY
 
Аватар для ChuwY
Поищите формулы в исходниках любого твинера с открытым исходным кодом.
Того же твинмакса.
Ну или у себя на компьютере, вот здесь:
Код:
C:\Program Files (x86)\Adobe\Adobe Flash CS5.5\Common\First Run\Classes\mx\transitions\easing
C:\Program Files (x86)\Adobe\Adobe Flash CS5.5\Common\Configuration\ActionScript 3.0\projects\Flash\src\fl\transitions\easing
C:\Program Files (x86)\Adobe\Adobe Flash CS5.5\Common\Configuration\ActionScript 3.0\projects\Flash\src\fl\motion\easing
Обновил(-а) ChuwY 06.03.2012 в 12:46
Старый 06.03.2012 12:42 iNils вне форума
iNils
 
Аватар для iNils
Цитата:
Поэтому придётся случайно узнать ещё об одной недокументированной особенности as3:
Надумано.
Цитата:
Это многоадресное событие, которое отправляется всеми экранными объектами, для которых зарегистрированы прослушиватели данного события.
Старый 06.03.2012 12:57 -De- вне форума
-De-
 
Аватар для -De-
А как при этом гарантируется, что по окончанию твина значение будет конечным? Твин в 3 кадра длиной там такое насчитает тогда.
Старый 06.03.2012 13:11 MikroAcse вне форума
MikroAcse
 
Аватар для MikroAcse
ChuwY, спасибо.
Старый 06.03.2012 13:58 BuKT вне форума
BuKT
 
Аватар для BuKT
Цитата:
Надумано.
Это в доках по событию так?

Цитата:
А как при этом гарантируется, что по окончанию твина значение будет конечным? Твин в 3 кадра длиной там такое насчитает тогда.
Я же не говорю, что вышеописанный код нужно использовать в реальных проектах. Ёжику ясно, что он не завершён. Но конкретные проблемы, описанные в сабже, в нём решены.
Старый 06.03.2012 16:13 iNils вне форума
iNils
 
Аватар для iNils
Цитата:
Это в доках по событию так?
Конечно.
Старый 06.03.2012 17:48 Zebestov вне форума
Zebestov
 
Аватар для Zebestov
Попытка реализовать интерполяцию циклом с инкрементами всегда сопряжена с накоплением погрешности. В данном случае все усугубляется еще и округлением до 1/20. Не надо так делать твины.
Цитата:
Ps: дайте ссылку на формулы easing.
MikroAcse, я же тебе уже давал хорошую ссылку
Старый 06.03.2012 23:25 MikroAcse вне форума
MikroAcse
 
Аватар для MikroAcse
Цитата:
MikroAcse, я же тебе уже давал хорошую ссылку
Знаю, я сделал свои формулы, потому что мой твин по другому реализован.
Кстати, я полностью переписал свой твин, теперь он работает со всеми объектами и в нем есть изинги, а еще он стал более усовершенствованным.
Старый 06.03.2012 23:38 dimarik вне форума
dimarik
 
Аватар для dimarik
Хоть кто-то справку читает!
Старый 07.03.2012 00:04 BuKT вне форума
BuKT
 
Аватар для BuKT
Спасибо за критику, исправил.
Старый 07.03.2012 00:14 dimarik вне форума
dimarik
 
Аватар для dimarik
Теперь ждем твининга невизуальных объектов.
Старый 07.03.2012 00:15 BuKT вне форума
BuKT
 
Аватар для BuKT
Цитата:
Теперь ждем твининга невизуальных объектов.
А разве не будет работать? (:
Код AS3:
public function SomeTweener(obj:Object, paramName:String, endValue:Number, frames:Number)
Старый 07.03.2012 01:07 MikroAcse вне форума
MikroAcse
 
Аватар для MikroAcse
Будет.
Старый 07.03.2012 20:49 MikroAcse вне форума
MikroAcse
 
Аватар для MikroAcse
Вообще я советую increment узнавать каждый раз.
По-моему, так точности больше, хотя точно я не знаю.
Старый 07.03.2012 21:19 BuKT вне форума
BuKT
 
Аватар для BuKT
Топик не про твининг =)
Старый 07.03.2012 21:56 dimarik вне форума
dimarik
 
Аватар для dimarik
Я вам один умный вещь скажу, но только вы не обижайтесь. Привяжите к текущему времени. Каждый следующи ENTER_FRAME лишь повод подсчитать реальное передвижение объекта с момента предыдущего вызова ENTER_FRAME. Нужен getTimer().
Старый 07.03.2012 21:57 MikroAcse вне форума
MikroAcse
 
Аватар для MikroAcse
Цитата:
Топик не про твининг =)
По названию не скажешь
Старый 08.03.2012 11:51 BuKT вне форума
BuKT
 
Аватар для BuKT
Цитата:
Нужен getTimer()
Зачем? В моём случае нужна анимация привязанная именно к кадрам.
Старый 08.03.2012 16:39 Zebestov вне форума
Zebestov
 
Аватар для Zebestov
BuKT, вот чем это сложнее твоего кода:

Код AS3:
import flash.display.Shape;
import flash.events.Event;
 
public class SomeTweener
{
	private var _obj:Object;
	private var _paramName:String;
	private var _startValue:Number;
	private var _endValue:Number;
	private var _duration:int;
	private var _frames:int;
	private var _eventDispatcher:Shape;
 
	public function SomeTweener(obj:Object, paramName:String, endValue:Number, duration:int)
	{
		_obj = obj;
		_paramName = paramName;
		_startValue = obj[paramName];
		_endValue = endValue;
		_duration = duration;
		_frames = 0;
		_eventDispatcher = new Shape();
		_eventDispatcher.addEventListener(Event.ENTER_FRAME, tween);
	}
 
	private function tween(e:Event):void
	{
		if (_frames == _duration)
		{
			_eventDispatcher.removeEventListener(Event.ENTER_FRAME, tween);
			return;
		}
		_frames++;
		_obj[_paramName] = _startValue + (_endValue - _startValue) * (_frames / _duration);
	}
}
И никакой головомороки с погрешностями.

P.S.
Для чистоты сравнения нарочно опустил в конструкторе проверки на наличие свойства paramName в obj (это необязательно, но хорошо бы) и на значение duration > 0 (можно конечно тупо втулить uint, но с некоторых пор я к нему с недоверием)

P.P.S.
Ну и неплохо бы наследоваться от EventDispatcher, чтобы по окончании твина кроме всего прочего еще и послать событие COMPLETE, хотя бы.
Старый 08.03.2012 19:53 MikroAcse вне форума
MikroAcse
 
Аватар для MikroAcse
Zebestov, огромное вам спасибо!!!!
Теперь я понял в чем разница между инкрементами и изингами!
Мой твин не понимал стандартные изинги потому, что работал с инкрементами.
+ мой твин работает на секундах (раньше работал на количестве движений) и из-за инкрементов я не мог понять, почему таймер работает неправильно (написал 1 секунда,а твин длился секунд 10).
Теперь и ваша ссылка пригодилась!
Старый 08.03.2012 21:59 BuKT вне форума
BuKT
 
Аватар для BuKT
Ничем не сложнее. Только я специально отметил, что опускаю множество маленьких и изящных улучшений, ибо написать твинер - не цель этого топика
 

 


Часовой пояс GMT +4, время: 07:30.


Copyright © 1999-2008 Flasher.ru. All rights reserved.
Работает на vBulletin®. Copyright ©2000 - 2019, Jelsoft Enterprises Ltd. Перевод: zCarot
Администрация сайта не несёт ответственности за любую предоставленную посетителями информацию. Подробнее см. Правила.