Once I was asked to deal with a bug: when changing frameRate in an arbitrary number of nested .swf, it began to behave strangely a handwritten “twinner” - a class that interpolates a certain value for a given time. Instead of his normal activities, the twiner could skip values, could stick on some one, and sometimes just at an arbitrary time point, set its final value to the variable and report on the completion of its work. The supplicant linked the problem with multi-level nesting and the mismatch of the own and parental fps.
I tried to write a twiner code from scratch and it turned out that my version also behaves strangely, despite the fact that there was only one level and fps was constant. In the process of solving the problem, I learned a couple of wonderful flash tricks, with which I hurry to share.
It would seem, what difficulties could there be? In the constructor of your own twiner, we pass the object reference, the parameter that needs to be changed, the final value and time / frames, for which this parameter should smoothly take the final view. For simplicity, take the case of a frame-by-frame value change:
')
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; } }
Yes, the easiest way to determine when it is time to update the value will be a subscription to the
Event.ENTER_FRAME event sent to the
DisplayObject constructor. However, we are not looking for easy ways and we make the twiner universal. That is, one that changes the parameters not only
DisplayObject 'as well. Therefore, you will have to use another
undocumented little-known feature as3:
Any DisplayObject that is not even added to the DisplayList regularly receives a frame entry event.
Change the constructor:
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 { } }
You can check it yourself. The
tween method will be regularly called with the fps frequency.
Go ahead. Again, in the simplest way - every call to
tween, we change the transmitted value by the same value (twinning type easeNone - that is, uniform). To do this, it is better to calculate the frame increment in the constructor, based on the difference between the final and starting values and the duration of the twin, and record the increment in the class field. In the
tween method itself, we will check how many frames the twinning already lasts and, upon reaching the specified value, interrupt twinning:
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--; } }
There is nothing wrong with using
frames as a counter for the remaining twinning ticks: we agreed not to use seconds as tween time. Moreover, except in the constructor, the
frames parameter is not used anywhere and no one (even we) will accuse us of changing the variable passed in by value for our own purposes.
It would seem - everything is twiner ready. And it will even work, and in most cases - also correctly. But there is one case where he will by no means give a normal result. I will give a small example:
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); } }
What will give us a trace as a result of calling this code? According to the logic of things - a column of forty numbers decreasing in increments of 0.05. In practice, any
DisplayObject still has at least one undocumented feature: its coordinates (and possibly some other properties) are always a multiple of 0.05. Attempting to assign a non-multiple value to them will fail: the next time it is drawn, it will be rounded to the nearest multiple of zero. In this particular example, this effect should not threaten us (in fact - it manifests itself in all its glory), but, for example, by increasing the value of frames during which the twinning should appear, up to 80, we will get an increment equal to 0.025 and the trace “will hang "At zero, never having reached -1.
There is another feature. In the
FlashPlayer runtime
environment, the
Number type is a 64-bit floating point number. Because of this, lining happens quite often. The easiest way to explain is by example:
trace(String(-.35 - .05))
This is how a flash behaves with all values of the
Number type (to which the
DisplayObject coordinate fields also belong), there's nothing you can do. It is quite natural that the example of the work of the traceSome method of our SomeClass class will fail even when twining into 40 frames. Practice has shown that
sprite.x will not be able to move exactly from the value of -0.35 being every frame before it rounded. The cycle is:
- Take the value of the object field (-0.35)
- Increment it by increment value (-0.35 + (-0.05) = -0.39 (9) 97)
- Write it in the object field (-0.39 (9) 97)
- (Hidden required item) The value of the DisplayObject instance coordinate field is rounded off (-0.35)
- Enter the next frame and take the value of the object field (-0.35)
- GOTO 2
Get rid of the hidden fourth point is impossible. However, the error caused by the symbiosis of math
Number and the restrictions of the values of the coordinates of the
DisplayObject cost us exactly three extra lines. To do this, you need to change the first and fifth points of the cycle and, instead of permanently “perishable” value storage, create your own:
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--; } }
It would seem, what have MVC? :-)
PS: Yes, the existing twiner code can and should be improved. Add a delayed launch, sending messages. You can add easing and other multifield twin. But this is not the case for this task, and therefore we will leave it as homework.