phaser icon indicating copy to clipboard operation
phaser copied to clipboard

[3.50] "delay" property in animation config results in uninterruptible/unstable animation

Open enderandpeter opened this issue 3 years ago • 9 comments

Version

  • Phaser Version: 3.54.0 (earliest version with issue: 3.50.0-beta.7)
  • Operating system: Any, but using Windows 10 right now
  • Browser: Firefox Any, but using Firefox right now

Description

When the delay property is set on an animation configuration, the animation is put in an unstable state where it is either not acknowledged or unable to be interrupted.

Expected

When setting the delay property on an animation config, the animation will be delayed the set number of milliseconds before starting. Also, when another animation is played, it will interrupt the animation currently being played.

Actual

When the delay property is set, the animation fails to load its initial frame when the game loads, and the animation cannot be interrupted by another.

Example Test Code

I was trying to explain this in the Discord forum, but I felt like no one knew what the heck I was talking about. And so plenty more details are here.

The problem can be seen in this GitLab project. It is a codebase that is gradually getting more sophisticated, but the general problem is exhibited.

The animations, such as the idle_left animation, generally look like this

import {Animations, Types} from 'phaser';

export default class Left extends Animations.Animation {
    static key = 'idle_left';
    constructor(manager: Animations.AnimationManager) {
        const { key } = Left;
        const config: Types.Animations.Animation = {
            key,
            frames: manager.generateFrameNames('atlas', {
                prefix: `player/mp_${key}_`,
                suffix: '.png',
                start: 1,
                end: 2,
                zeroPad: 2
            }),
            frameRate: 10,
            repeat: -1,
            repeatDelay: 5000,
            delay: 3000,
            yoyo: true
        };
        super(manager, key, config);
    }
}

Please try deploying this project to see what I am talking about. Instructions are in the readme. Basically, clone the repo, install with yarn or npm, copy the example config file to a real config file, and run yarn start or npm start in the app folder. This project was scaffolded with Create React App and it uses TypeScript.

I tried to keep things generally organized. The runnable app is in the app folder. Assets the project needs, like spritesheets and meta data, are in app/public/assets. Everything is organized in classes. Sprites are in app/src/Sprites, scenes are in app/src/Scenes, and animations are in app/src/animations. Of particular note are the Player animations.

Please let me know if you have any questions.

Additional Information

When the Idle and Walk animations work properly, the character moves like this:

Proper-Animation-Delay

Note how when the page initially loads, the frame that loads is the first frame in the idle_right animation. Also, there is a few seconds of delay before the character blinks (the Idle animation). Note how after making the character move in either direction and stopping, again there is a few seconds of delay before the Idle animation, and the character is facing in that direction. This is how this is supposed to work.

When using at least Phaser 3.50.0-beta.7, this happens:

https://user-images.githubusercontent.com/4011193/116803545-4c083500-aade-11eb-9b1a-57da270abe82.mp4

The video file size is much smaller as an MP4 than an animated GIF.

Note how the Character sprite starts with the first sprite listed in app/public/assets/atlas.json instead of the first sprite of the idle_right animation. After a few seconds, you will see that it does start playing the idle animation properly. But when you move the character, it is frozen in a frame, I'm not sure exactly which one, I think the first or last one played in the idle animation. Then you will see how at one point, the walking animation suddenly plays, but I am not even pressing anything. This behavior is actually a fluke that only occurred when I used a new screen recording program. The typical behavior is that when I move the character to the left, they remain facing to the right and the walking animation does not play. After a few seconds, the idle_left animation plays but the delay seems a little longer. Anyway, as you can see, things are clearly not working.

However, if the delay property is removed from the Idle animations, you will see this:

https://user-images.githubusercontent.com/4011193/116803650-4ced9680-aadf-11eb-94bd-1891868fb2da.mp4

Everything is pretty much normal now, but only because the delay for the idle animation has been commented out. You will see how this restored the first frame in the idle animation to load this time at the start of the scene. But since the delay property is removed, the character starts blinking immediately instead of the delay.

Can anyone help determine why this is happening? Why does the delay property on an animation config cause this broken behavior in newer Phaser?

Again, the earliest version on which I could confirm this issue occurs was 3.50.0-beta.7. The latest version that did not have this issue was 3.50.0-beta.2. I could not get 3.50.0-beta.4 to load. It gave me the dreadful "Phaser could not be found" error.

enderandpeter avatar May 02 '21 05:05 enderandpeter

Note how the Character sprite starts with the first sprite listed in app/public/assets/atlas.json instead of the first sprite of the idle_right animation. After a few seconds, you will see that it does start playing the idle animation properly.

I think that's expected. If a delay is set, the first frame shouldn't show until the delay has expired.

But when you move the character, it is frozen in a frame, I'm not sure exactly which one, I think the first or last one played in the idle animation. Then you will see how at one point, the walking animation suddenly plays, but I am not even pressing anything.

I think that may be a bug. Try instead

sprite.anims.playAfterDelay('walk_right', 0);

samme avatar May 02 '21 16:05 samme

sprite.anims.stop().playAfterDelay('walk_right', 0);

samme avatar May 02 '21 18:05 samme

Using the first frame in the animation was a feature that made a lot of sense in previous Phaser. I can work with that if need be because it will still load what is provided to Sprite.setFrame, but then the sprite shows up in a different position than when it loads the frame from the animation for some reason.

playAfterDelay is something I did try earlier just to experiment, but it does not address the issue because the problem is that the walk animation is not interrupting the idle animation. So it is not that the walk animation should be played after the delay, rather the walk animation should play immediately upon request while the idle animation is playing, so as to interrupt the idle animation and play the walk animation instead, as seen in the animated gif.

I also tried AnimationManager.addMix but that does not have the right effect either. I have a strong feeling that something has fundamentally changed in the framework that has inadvertently effected the original behavior.

enderandpeter avatar May 02 '21 18:05 enderandpeter

I mean the second thing you described is probably a Phaser bug. Here is the unwanted delay problem: https://codepen.io/samme/pen/GRrVjoG

I think it happens because AnimationState#delayCounter isn't cleared when switching animations.

A workaround in that example is

this.rob.anims.stop().playAfterDelay("death", 0);

samme avatar May 02 '21 20:05 samme

Thanks for looking into this. I am trying that workaround but I am not seeing it work like the previous behavior. In my Character.ts class, I've changed the playAnimation method to look like this:

public playAnimation(animation: CharacterAnimationKey){
    if(animation === 'idle_left' || animation === 'idle_right'){
        this.anims.stop().playAfterDelay(animation, 3000);
    } else {
        this.anims.play(animation, true);
    }
}

Although I do see the idle animation play after the time specified, the walk animations do not play when moving and it is once again stuck at the first idle animation frame as you move. And it still loads the door frame for the player when the scene first loads. And so, I hope I can try to help determine what caused this between 3.50.0-beta.2 and 3.50.0-beta.7.

enderandpeter avatar May 08 '21 21:05 enderandpeter

You would have to use the workaround, .stop().playAfterDelay(key, 0) instead of play() for each animation, every time, I think.

And it still loads the door frame for the player when the scene first loads.

I think that's just how delayed animations work. You need to use setFrame() if you want to display a frame before the animation starts.

samme avatar May 11 '21 16:05 samme

Calling .stop().playAfterDelay for each animation every time is most certainly not what I want. I'm just looking for the delay feature to work for animations that need a delay, not every animation.

I assure you, delayed animations previously worked exactly as depicted in the animated gif, where the initial frame was set from the idle_right animation. It seems to me that the delay feature being broken has a lot to do with the previous initial frame behavior not working as well.

enderandpeter avatar May 15 '21 03:05 enderandpeter

The unwanted delay needs a bug fix, but I'm not sure where AnimationState#delayCounter should be reset.

I think the initial frame behavior you're seeing is probably correct since v3.50. You might be able to imitate the previous behavior by removing your animation delays and using instead:

this.anims.play('key', true);
this.anims.nextTick += DELAY;

samme avatar May 15 '21 04:05 samme

This has all been rather confusing, but I appreciate you looking into this so much.

I've been examining your example more and I do agree with calling the main issue "unwanted delay", because another animation will not interrupt the currently playing one immediately. Instead, there is a few seconds of unwanted delay. Also, I am seeing how you wrote an assertion to check the value of AnimationState#delayCounter and how it is not resetting unless you do the .stop().playAfterDelay('key', 0) workaround, which also results in the first animation being interrupted as expected.

I have returned to my project and updated it to Phaser 3.55.2. I'm still confused about how this engine will now set a sprite with an animation delay to the first sprite in the atlas. I am indeed calling Sprite.setFrame in my project. Take a look at how all Sprites are using the Entity class and this class always sets a default frame when the object is instantiated. So, as soon as an object is made for a sprite, the constructor will set a default frame. That frame ends up getting replaced by the very first frame in the atlas during the animation delay.

I did try the last workaround you suggested that involved removing the animation delay and using Animation.nextTick instead. Fortunately, that is indeed working and now the playable character has its original animation behavior restored.

enderandpeter avatar Dec 30 '21 23:12 enderandpeter

The delay issue is now fixed in the master branch. I've also added a new config property showBeforeDelay which, as the name implies, will set the first frame immediately and not after the delay expires.

photonstorm avatar Oct 31 '22 18:10 photonstorm