📜 ⬆️ ⬇️

An example of caching software animations in Flash

When developing applications using Flash technology, to a greater extent this applies to games with a large number of graphics and animations, in the end, you can come to the conclusion that FPS will stop somewhere at the level of 2-3. This means that it is time to do optimization. At the same time, it is necessary to optimize first of all what will ultimately affect the system performance. Acceleration of work due to optimization at the logic level is specific and depends on the specific application. But the acceleration of animations can be successfully used in many projects, and it will be discussed.

About the method of caching animated clips has already been said in this article.
Let me briefly remind you that the point was to pre-rasterize the animation into objects like BitmapData and then draw with them already. At the same time, CPU time is spent only on drawing raster graphics, which in practice allows for significant performance. However, this method does not always work.

If, however, the effect is implemented using ActionScript (for example, a particle system), then a cached MovieClip can contain only one frame. In this case, this method requires refinement.

Decision


The point is to cache frames not in advance, but during the first play of the effect (“lazy loading”). On subsequent runs, the data for rendering will already be taken from BitmapData objects. In addition, with this approach, it will be possible to avoid the delay in loading a movie, which occurs during preliminary caching (especially with a large number of nested clips).
')
So, we don’t know the exact number of frames for rasterization, it is necessary to specify it manually, or calculate it in some other way. For example, when using a particle system, the required number of frames can be roughly learned from information about the lifetime of the particles. However, this already depends on the specific situation and is not considered in this article.

Main class code for caching:

public class MyCache extends Sprite
{
private var _clip: Sprite = null ;

public function MyCache(clip: Sprite, framesCount: int )
{
_clip = clip;
_framesCount = framesCount;
_bitmap = new Bitmap( null , PixelSnapping.AUTO, true );
addChild(_bitmap);
mouseEnabled = false ;
}

private var _bitmap: Bitmap = null ;

private var _frames: Array = new Array();
private var _framesCount: int = 0;
private var _currentFrame: int = 0;
private var _onEnd: Function = null ;
private var _loop: Boolean = true ;

public function play(loop: Boolean = true , onEnd: Function = null ): void
{
_currentFrame = 0;
_loop = loop;
_onEnd = onEnd;
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

public function stop(): void
{
removeEventListener(Event.ENTER_FRAME, onEnterFrame);
if (_onEnd != null )
_onEnd( this );
}

private function createCache(clip: Sprite): Object
{
var rect: Rectangle = clip.getRect(clip);
var bmpData: BitmapData = new BitmapData(rect.width, rect.height, true , 0x00000000);
var m: Matrix = new Matrix();
m.translate(-rect.x, -rect.y);
m.scale(clip.scaleX, clip.scaleY);
bmpData.draw(clip, m);

return { "frame" : bmpData, "x" : rect.x, "y" : rect.y };
}

private function cachNextFrame(): void
{
if (_frames.length < _framesCount)
{
_clip.dispatchEvent( new Event(Event.ENTER_FRAME));
_frames.push(createCache(_clip));
}
}

private function onEnterFrame(e: Event): void
{
cachNextFrame();

if (_currentFrame < _frames.length)
{
var bmpData: Object = _frames[_currentFrame];
_bitmap.bitmapData = bmpData.frame;
_bitmap.x = bmpData.x;
_bitmap.y = bmpData.y;
}
else
{
if (_loop)
_currentFrame = 0;
else
stop();
}
_currentFrame++;
}
}

* This source code was highlighted with Source Code Highlighter .
public class MyCache extends Sprite
{
private var _clip: Sprite = null ;

public function MyCache(clip: Sprite, framesCount: int )
{
_clip = clip;
_framesCount = framesCount;
_bitmap = new Bitmap( null , PixelSnapping.AUTO, true );
addChild(_bitmap);
mouseEnabled = false ;
}

private var _bitmap: Bitmap = null ;

private var _frames: Array = new Array();
private var _framesCount: int = 0;
private var _currentFrame: int = 0;
private var _onEnd: Function = null ;
private var _loop: Boolean = true ;

public function play(loop: Boolean = true , onEnd: Function = null ): void
{
_currentFrame = 0;
_loop = loop;
_onEnd = onEnd;
addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

public function stop(): void
{
removeEventListener(Event.ENTER_FRAME, onEnterFrame);
if (_onEnd != null )
_onEnd( this );
}

private function createCache(clip: Sprite): Object
{
var rect: Rectangle = clip.getRect(clip);
var bmpData: BitmapData = new BitmapData(rect.width, rect.height, true , 0x00000000);
var m: Matrix = new Matrix();
m.translate(-rect.x, -rect.y);
m.scale(clip.scaleX, clip.scaleY);
bmpData.draw(clip, m);

return { "frame" : bmpData, "x" : rect.x, "y" : rect.y };
}

private function cachNextFrame(): void
{
if (_frames.length < _framesCount)
{
_clip.dispatchEvent( new Event(Event.ENTER_FRAME));
_frames.push(createCache(_clip));
}
}

private function onEnterFrame(e: Event): void
{
cachNextFrame();

if (_currentFrame < _frames.length)
{
var bmpData: Object = _frames[_currentFrame];
_bitmap.bitmapData = bmpData.frame;
_bitmap.x = bmpData.x;
_bitmap.y = bmpData.y;
}
else
{
if (_loop)
_currentFrame = 0;
else
stop();
}
_currentFrame++;
}
}

* This source code was highlighted with Source Code Highlighter .

In order to correctly position the bitmap, for each frame, in addition to the picture itself, its offset is additionally stored.

As mentioned above, caching is performed at the first stage of rendering and the performance increase will be noticeable only on subsequent launches. Therefore, after the animation is completed, the object should not be deleted, but saved to the pool for later use.

Base pool class

public class MyCachePool
{
protected function createEffect(): MyCache
{
return null ;
}

private var _pool: Array = new Array();

public function show(owner: Sprite, px: int , py: int , loop: Boolean = true ): void
{
var effect: MyCache = getEffect();

owner.addChild(effect);
effect.x = px;
effect.y = py;
effect.play(loop, onEnd);
}

private function getEffect(): MyCache
{
if (_pool.length > 0)
{
var idx: int = Math.round((Math.random() * (_pool.length - 1)));
var rez: MyCache = _pool[idx];
_pool.splice(idx, 1);
return rez;
}

return createEffect();
}

private function onEnd(effect: MyCache): void
{
_pool.push(effect);
}
}

* This source code was highlighted with Source Code Highlighter .
public class MyCachePool
{
protected function createEffect(): MyCache
{
return null ;
}

private var _pool: Array = new Array();

public function show(owner: Sprite, px: int , py: int , loop: Boolean = true ): void
{
var effect: MyCache = getEffect();

owner.addChild(effect);
effect.x = px;
effect.y = py;
effect.play(loop, onEnd);
}

private function getEffect(): MyCache
{
if (_pool.length > 0)
{
var idx: int = Math.round((Math.random() * (_pool.length - 1)));
var rez: MyCache = _pool[idx];
_pool.splice(idx, 1);
return rez;
}

return createEffect();
}

private function onEnd(effect: MyCache): void
{
_pool.push(effect);
}
}

* This source code was highlighted with Source Code Highlighter .


This class is basic. For a specific effect, you must inherit a new class and override the createEffect function, as indicated below.

The derived class of the pool for a particular effect.

public class BoomCachPool extends MyCachePool
{
override protected function createEffect():MyCache
{
return new MyCache( new gBoom(), 20);
}
}


* This source code was highlighted with Source Code Highlighter .
public class BoomCachPool extends MyCachePool
{
override protected function createEffect():MyCache
{
return new MyCache( new gBoom(), 20);
}
}


* This source code was highlighted with Source Code Highlighter .


The gBoom class is a MovieClip exported to a project from a swc file. The number of frames for caching was chosen experimentally.

Below are the test results.



Figure 1 - Particles without caching


Figure 2 - Particles with caching

This method also has certain disadvantages. At the first stage, the FPS is the same as without using caching (in theory, even less, because the rasterization in BitmapData is additionally performed). In addition, in contrast to the use of the original clip, the number of various effects here is still finite, because Subsequent effects are already taken from the pool. However, this is a necessary evil to increase productivity (you always have to sacrifice something). To compensate for this, the effects from the pool are not taken sequentially, but randomly. In practice, this is often enough to ensure user-friendly variety.

Conclusion


The source code for the article should not be considered as a complete solution. For clarity, it is missing the check for exceptional situations and, in general, for full use, it requires revision. In addition, in the case of using any particle system, an example will need to be adapted. For example, when using Partigen (the code was originally developed to optimize it), it is necessary to manually start and stop emitters (there are also several other nuances, but they are already beyond the scope of this article). However, in general, tests show that caching by software effect allows for a significant increase in performance and is often the only way to ensure a user-friendly application speed.

The source code of the project can be downloaded here .

PS: The above described primarily relates to effects based on a particle system. In the case of frame animation, there is no need to re-cache when creating a new object, since within one frame the picture is static. It is more correct to save frames once in a publicly accessible place (for example, in a static array), and then when drawing all the clips of the clip, take them from there. So it will be most beneficial in terms of performance and memory consumption.

Links by topic:

Source: https://habr.com/ru/post/113319/


All Articles