OpenFL animation in preloader always stutters first time

166 views Asked by At

Im using the built in NMEPReloader as my base class for my preloader in openfl.

What Im doing is playing an animation frame by frame

class AttractAnimation extends Sprite
{
var currentFrame:Int;
var previousFrame:Int;
var bitmapFrames:Array<Bitmap>;
var loadScreenTimer:Timer;

public function new()
{
    super();
    currentFrame = 0;
    previousFrame = -1;
    bitmapFrames = new Array<Bitmap>();
}

public function assignBitmapData(bitmapData : Array<BitmapData>){
    for(bmap in bitmapData){
        var frame = new Bitmap( null, flash.display.PixelSnapping.AUTO, true );
        frame.bitmapData = bmap;
        pushFramesAsBitmap(frame);
    }
}
public function pushFramesAsBitmap(frameAsBitmap:Bitmap) {
    bitmapFrames.push(frameAsBitmap);

    frameAsBitmap.visible = false;
    addChild(frameAsBitmap);
}

public function startAnimation(fps:Int) {
    var milliseconds:Float = 1000 / fps;
    loadScreenTimer = new Timer(milliseconds, 10);
    loadScreenTimer.addEventListener(TimerEvent.TIMER, updateAnimation);
    loadScreenTimer.start();
}

    private function updateAnimation(e : TimerEvent):Void {
        if (currentFrame > bitmapFrames.length -1) { currentFrame = 0; }
        if (previousFrame != -1) {
            bitmapFrames[previousFrame].visible = bitmapFrames[previousFrame].__combinedVisible = false;
        }
        bitmapFrames[currentFrame].visible = bitmapFrames[currentFrame].__combinedVisible = true;
        previousFrame = currentFrame;
        currentFrame++;
    }

}

what im seeing when I call playAnimation is it stutters really bad, the next time I play the animation it plays smoothly.

I have tried setting the alpha to 0 playing the animation then setting it back to 1 and playing but it will stutter the second time in this case.

Same if I move it behind another display object play the animation then move it to the front of the draw order, it will stutter the second time.

IS there a way I can play this animation without it stuttering?

2

There are 2 answers

0
Derek Lawrence On BEST ANSWER

I was able to stop the stuttering by using a spritesheet instead of individual images.

I changed the name as we added a second animation. You can see I use Tilesheet now instead of a Bitmap array.

I have updated the below answer again to include the suggested onEnterFrame change. The spritesheet uses got rid of the stuttering but the animation is now much smoother with enterframe event.

class AnimatedSprite extends Sprite
{
public var currentFrame:Int;

private var loadScreenTimer:Timer;
private var tiles: Array<Int>;
private var tilesheet : Tilesheet;

private var fps : Int;
private var mDeltaTime:Float = 0; //the current delta time for time-based updates
private var mTime:Float; //a variable containing the current time since the last frame change


public function new(framesPerSecond : Int, looping : Bool = false)
{
    super();
    currentFrame = 0;
    fps = framesPerSecond;
}

//public methods
public function assignBitmapData(bitmapData : BitmapData, tileData : Array<Rectangle>) : Void {
    tilesheet = new Tilesheet(bitmapData);
    tiles = new Array<Int>();

    for(i in 0...tileData.length){
        tiles.push(tilesheet.addTileRect(tileData[i]));
    }
    mTime = 0;
}

public function playAnimation() : Void {
    trace("playAnimation");
    visible = true;
    currentFrame = 0;
    addEventListener(Event.ENTER_FRAME, onEnterFrame, false, 0, true);
}

//private methods
private function endAnimation(?e : TimerEvent) : Void {
    removeEventListener(Event.ENTER_FRAME, onEnterFrame);
    graphics.clear();
    trace("end animation");
    currentFrame = 0;
    mTime = 0;
}

private function onEnterFrame(e:Event):Void
{
    trace("enter frame");
    var currentTime = Lib.getTimer(); //get the current time
    var elapsed:Float = (currentTime - mDeltaTime); //calculate the time since the last update
    mDeltaTime = currentTime;

    mTime += elapsed; //add the elapeed time to mTime
    if (mTime >= 1000 / fps) //if it is time to change frame...
    {
        mTime = 0;
        graphics.clear();
        tilesheet.drawTiles(this.graphics, [0, 0, tiles[currentFrame], 1, 1]);
        trace("Draw frame : " + currentFrame);
        if (currentFrame == tiles.length-1) endAnimation();

        currentFrame++;
    }
}
}
2
oli_chose123 On

Since your implementation is pretty simple and does not depend on much external APIs, the timer could be a cause. Remember that a preloader is actually loading everything in priority, so lagging is to be expected unless you take steps to either hide it or prevent it. It is quite possible that the timer event fires multiple times in sucession because of a lag. This would result is visual bugs and stuttering.

First, Try first using an enterFrame event listener instead of a timer, which is a bad design choice in most if not all situations.

//Using this event listener
addEventListener(Event.ENTER_FRAME, OnUpdateAnimation);

//which would call this function every frame
function OnUpdateAnimation(e:Event):Void
{
    //calculate delay since last frame
    //play next frame
}

Second, instead of using an array of bitmaps, use a single bitmap and an array of bitmapdata. While for a small animation that shouldn't change much, it is a better practice and could help with performance (for example, in a preloader).

Here's a new implementation of an animation class extending sprite. It has not been tested but should work as advertised. Note that usually, using an EnterFrame listener per object is not a good practice; you would usually have a main update function that would loop over all updatable objects. But is this case, it's good enough.

class Animation extends Sprite 
{

    private var mBitmapDataList:Array<BitmapData>; //a list of bitmapdata
    private var mBitmap:Bitmap; //the bitmap that will contain all bitmapdata
    private var mCurrentIndex:Int; //the currently displayed bitmapdata
    private var mFrameLength:Float; //the duration of a frame in miliseconds
    private var mDeltaTime:Float = 0; //the current delta time for time-based updates

    private var mTime:Float; //a variable containing the current time since the last frame change
    public function new(aFrameLength:Float) 
    {
        super();
        mFrameLength = aFrameLength;
        mBitmapDataList = new Array<BitmapData>();
        mBitmap = new Bitmap();
        addChild(mBitmap); //add the bitmap to the scene 
                        //(it will display nothing as no bitmapdata is set)
        mCurrentIndex = 0;
    }

    //add a bitmapdata instead of a complete bitmap object
    public function addBitmapData(aBD:BitmapData):Void
    {
        mBitmapDataList.push(aBD);
    }

    public function play():Void
    {
        addEventListener(Event.ENTER_FRAME, onEnterFrame, false, 0, true);
    }

    private function onEnterFrame(e:Event):Void 
    {
        var currentTime = Lib.getTimer(); //get the current time
        var elapsed:Float = (currentTime - mDeltaTime); //calculate the time since the last update
        mDeltaTime = currentTime;

        mTime += elapsed; //add the elapeed time to mTime
        if (mTime >= mFrameLength) //if it is time to change frame...
        {
            mTime = 0;
            mCurrentIndex++;
            if (mCurrentIndex == mBitmapDataList.length) mCurrentIndex = 0;
            mBitmap.bitmapData = mBitmapDataList[mCurrentIndex]; //change it
        }
    }
}

This way, even in an extreme case of stuttering, only the FIRST frame will be skipped. Note, also, that on some targets, you will not be able to display images as they are not yet loaded. An alternative solution is to not embed assets <assets path="assets" embed="false"/> and load them using OpenFL' Asset class (and thus forgo the preloader altogether, a gimicky thing in my humble opinion)

PS: please excuse my m and a prefixes on variables. m is for member variables and a for arguments.