pixijs / spine

Pixi.js plugin that enables Spine support.
Other
564 stars 217 forks source link

setAnimation doesn't work immediately after adding an already-removed child back on the stage #159

Open jmlee2k opened 7 years ago

jmlee2k commented 7 years ago

First, thanks for all the great work here!

It seems that performing the following sequence of events will cause a spine animation not to play the second time:

  1. add a spine object to the stage
  2. play an animation on the spine object (this works as expected)
  3. wait some length of time, via setTimeout, a complete listener on the spine object, or any other method.
  4. remove the child from the stage
  5. wait another length of time
  6. add the child to the stage, and play the same animation as in step 2 (nothing appears to happen).

In addition, the same thing happens when setting visible to true and false instead of removing and adding the spine object to/from the stage.

I've managed to get it to work by either waiting around 20ms between showing the animation the second time and playing it, or by using requestAnimationFrame. However, both of these methods leave the animation at the last frame for a split second before playing it again.

My guess is that this is some sort of optimization to prevent animations from playing on objects which aren't on the stage or hidden, and it doesn't know that the object is visible / back on the stage until the next JS frame.

I know this is a bit complicated, so I've created a test case which displays the issue at https://jmlee2k.github.io/pixi-spine-anim-test/. You can also view the source at https://github.com/jmlee2k/pixi-spine-anim-test.

Each button will:

  1. add the spine object and play the animation (which works as expected)
  2. wait 2000ms
  3. hide the spine object (depending on the button pressed, this will either remove the spine object from the stage or set it's visibility to false)
  4. wait 1000ms
  5. show the spine object (depending on the button pressed, this will either add the spine object on the stage, or set it's visibility to true)
  6. play the animation again (which will only work if selecting "timeout" or "RAF" from the dropdown.

I've tried to keep the test case as simple as possible, so please excuse the somewhat brutal code :).

Please let me know if you need any other information.

Thanks in advance!

albymack commented 7 years ago

The reason this breaks is because pixi spine is doing calling update () in the autoUpdateTransform function. Pixi.js is looping through it's children during its updateTransform function, so if you add/remove children during that loop, things break. In this case, the autoUpdateTransform calling update() causes events to fire, and if you remove/add children it will break pixi.js loop.

So there's 2 solutions. 1) set the spine anim autoUpdate = false, and then call update manually yourself somewhere in your game loop.

2) Fix pixi spine so it uses PIXI.js shared ticker instead of calling update () during the autoUpdateTransform function.

something like this.

    Object.defineProperty(Spine.prototype, "autoUpdate", {
        get: function () {
            return (this._setTicker);
        },
        set: function (value) {
            this.updateTransform = PIXI.Container.prototype.updateTransform;
            if (value)
            {
                if (!this._setTicker)
                    PIXI.ticker.shared.add (this.updateTick, this);
            }
            else
            {
                PIXI.ticker.shared.remove (this.updateTick, this);
            }
            this._setTicker = value;
        },
        enumerable: true,
        configurable: true
    });
    Spine.prototype.updateTick = function (tickerDeltaTime)
    {
        this.update (tickerDeltaTime / PIXI.ticker.shared.speed / PIXI.settings.TARGET_FPMS * 0.001);
    }

autoUpdateTranform function isn't used at all in this example modification.

albymack commented 7 years ago

You'll probably also want to listen for 'added" and 'removed' events from pixi and add/remove the shared ticker handler as appropriate so it isn't updating spine animations that isn't added to the scene.

jmlee2k commented 7 years ago

Thanks, If you make a PR, I'll be happy to try it out to see if it works.

ivanpopelyshev commented 7 years ago

The problem is that pixi has no "added to stage" or "removed from stage" events.

Vote for it: https://github.com/pixijs/pixi.js/issues

Until i fixed, you have to do it on your own. autoUpdate works only in simple cases :(

AndreyGalaktionov commented 6 years ago

Any updates ?

jeremygillespiecloutier commented 6 years ago

Does anyone have a complete working fix for this? I need to make my spine visible and start an animation in the same frame, but this does not seem possible. @albymack Do you have the complete code including the "added" and "removed" events that I could use as a workaround for this bug? I've been stuck on this for quite a while and haven't really made any progress. Thanks!

ivanpopelyshev commented 6 years ago

Which version do you use ? try update to latest (from bin folder of this repo)

but yeah, nobody solved it, and there's no animation system in pixi, its user job to implement it for their app.

ivanpopelyshev commented 6 years ago

good idea :)

jeremygillespiecloutier commented 6 years ago

I changed updateTick to the following and it seems to work, now it only updates when the spine is visible:

        Spine.prototype.updateTick = function (tickerDeltaTime){
            if(this.worldVisible){
                this.update (tickerDeltaTime / PIXI.ticker.shared.speed / PIXI.settings.TARGET_FPMS * 0.001);
            }
        };
jeremygillespiecloutier commented 6 years ago

I had some issues with the fix above that caused flickering on screen, but I think I found another way to solve the problem. I only tried it on a test case so far and it seems to work but I will try it on my main project tomorrow.

var oldRender=PIXI.Application.prototype.render;
var defferedAnimations=[];

PIXI.Application.prototype.render=function(){
    oldRender.apply(this, arguments);
    for(var i=0;i<defferedAnimations.length;i++){
        var anim=defferedAnimations[i];
        anim.func.apply(anim.scope, anim.args);
    }
    defferedAnimations=[];
};

var statePrototype=pixi_spine.core.AnimationState.prototype;
var oldAnim=statePrototype.setAnimation;

statePrototype.setAnimation=function(){
    defferedAnimations.push({func:oldAnim, scope:this, args:arguments});
};

Essentially I defer calls to the setAnimation function so that it only gets executed at the end of PIXI's rendering phase (after it has finished calling updateTransform on everything).