[SOLVED] Playing animations with Anim, using code

Hi

I’m trying to setup a simple controller that using two keys - will play an animation either forwards or backwards. All using Anim component.

Can’t get my head around it to work in good way. My initial tests has been with a simple 1 state anim state graph and then controller code that will set the speed to either 1, -1 or 0 depending on a keypress. Like so:

code: this.animComponent.speed = 1 // Set forward speed

Is this a good practice?

It kind of works, but I run into some issues:

  • If I press A key to forward the animation all the way to it’s end. I can no longer run it backwards from there. It stops.
  • If I run it backwards, I can see the activeStateCurrentTime goes below zero. And if I want to forwards from there I need to first wind it up to 0 again before it starts moving. Does it need to be clamped manuallu to the range of the duration?

Regards Björn

Hi @bjorn.syse!

What if you set loop to true?

If I set the state to loop, the animation will snap to frame 1 when I reach the end. Not my intention.

Alright. There are a couple of forum topics about playing animation in reverse. At least the topic below looks related. It seems the GitHub issue about it has still an open status.

Good find, it looks like exactly my issue.

Wonder if there is a workaround I can do in the meanwhile.

Check the topic below, they share a workaround for the animation component, but it will probably work for the anim component as well.

Thanks, I’ve been experiencing with fractional values a bit from the end and start, to try to cap it but so far (using 1 - 0.01 and 1 - 0.05) it just ended up snapping to 1 anyway. maybe AnimationComponent will behave differently.

Seems my monitoring might have been what was wrongly implemented. I can confirm that if I monitor in update loop, I get it to workaround the bug where the animation stops and become unplayable if it ever reaches 100%.

// Update method to monitor animation progress
AnimationController.prototype.update = function (dt) {
    if (this.animComponent && this.animComponent.baseLayer) {
        const currentTime = this.animComponent.baseLayer.activeStateCurrentTime;
        const duration = this.animComponent.baseLayer.activeStateDuration;

        // Prevent the animation from reaching 100%
        if (currentTime / duration > 0.99 && this.animComponent.speed > 0) {
            this.animComponent.speed = -this.speed; // Reverse the animation
            console.log(`Reversing animation to prevent reaching 100%`);
        } else if (currentTime / duration < 0.01 && this.animComponent.speed < 0) {
            this.animComponent.speed = this.speed; // Play forward again
            console.log(`Playing animation forward to prevent freezing`);
        }
    }
};

I don’t know, I spoke too soon I think., Still fighting with this silly issue.

Seems that this.animComponent.baseLayer.activeStateCurrentTime report NaN in certain cases. Anim system is probably very competent and flexible but when I’ve been fighting with it for two straight days just to play or reverse an animation with code I feel it’s convoluted. Going to try the legacy system and see how it behaves.

For something like this, I would not use the graph as-is and instead invoke the transition via code using AnimComponentLayer | Engine API Reference - v2.6.1

Thanks for chirping in @yaustar, really appreciate it!

What I’m trying to do is to set up an interactive robotarm controller for this asset. I tried with physics package and motors/constraints but it was a trip down crazy lane. Found a much simpler approach just rotating the parts of the hierarchy and it’s very stable.

chrome_ueeYAWVhM7

For this final joint/gripper however - the movement is so complex it have been rigged and animated in blender - and my simple goal is to play that animation forwards when a certain key is pressed, and backwards when another key is pressed.
blender_tp9aMbXQAD

It feels like such a simple thought so it’s very frustrating I can’t figure out how to do it.

I just tried the transition code you linked to in an old thread, but that one just plays a certain animation in a state. Not sure it solved my issue alltogether, does it?

If all you need to play is a single animation, what @yaustar is suggesting is a good option. Simply play the animation from script, avoid the state graph completely.

If there is problem with playing it backwards, you could consider exporting another animation that is time-reversed, and playing this from code as needed as well.

You probably need to add the offset of the current progress when calling transition

eg PlayCanvas 3D HTML5 Game Engine

Use keys 1 and 2 for forward and back

There is a few bugs but you get the idea

1 Like

Thank you, this is really quite helpful!

I’m now experimenting with the activeStateProgress value, but can’t really understand it. Sometimes it returns negative values, is that expected?

I’ve established two states in the state graph just like you and used the same animation but set the speed to -1 for the backwards state. Perhaps that’s what’s messing up the progress, have no clue.

Was going to try the activeStateCurrentTime instead, but that one also report negative time values in certain cases.

So this is my latest setup, just to tie it all together. This is now working quite good, and what I had to do in the end was to save my own “Current Progress” using the absolute value. I have some oddities when it comes to boundary conditions left to solve.

State graph:

Code:

var AnimationControllerTransition = pc.createScript('animationControllerTransition');

// initialize code called once per entity
AnimationControllerTransition.prototype.initialize = function() {
    
    this.entity.anim.speed = 0;
    this.currentProgress = 0;
};
//  update code called every frame
AnimationControllerTransition.prototype.update = function(dt) {

    if (this.app.keyboard.isPressed(pc.KEY_1)) {
        if (!this.isPlaying) {
            this.isPlaying = true;
            console.log(`Playing forwards from ${this.currentProgress.toFixed(4)}`);
            this.entity.anim.speed = 2;
            this.entity.anim.baseLayer.transition('Forwards', 0, this.currentProgress);

        }
    }

    if (this.app.keyboard.wasReleased(pc.KEY_1)) {
        this.entity.anim.speed = 0;
        this.isPlaying = false;
        this.currentProgress = Math.abs(this.entity.anim.baseLayer.activeStateProgress);
        console.log(`Paused ${this.currentProgress.toFixed(4)}`);
    }


    
    if (this.app.keyboard.isPressed(pc.KEY_2)) {
        if (!this.isPlaying) {
            this.isPlaying = true;
            console.log(`Playing backwards from ${this.currentProgress.toFixed(4)}`);
            this.entity.anim.speed = 2;
            this.entity.anim.baseLayer.transition('Backwards', 0, this.currentProgress);
            
        }
    }

    if (this.app.keyboard.wasReleased(pc.KEY_2)) {
        this.entity.anim.speed = 0;
        this.isPlaying = false;
        this.currentProgress = Math.abs(this.entity.anim.baseLayer.activeStateProgress);
        console.log(`Paused ${this.currentProgress.toFixed(4)}`);
    }

    if (this.isPlaying) {

        if (!this.lastLogTime || (Date.now() - this.lastLogTime) > 100) {
            console.log(`progress: ${this.entity.anim.baseLayer.activeStateProgress.toFixed(2)}. time: (${this.entity.anim.baseLayer.activeStateCurrentTime.toFixed(2)}/${this.entity.anim.baseLayer.activeStateDuration.toFixed(2)}`);
            this.lastLogTime = Date.now();
        }
    }

};

Behaviour:

Honestly, don’t know and don’t really have the time to find out. Chances are yes it’s expected because the state speed is negative :sweat_smile:

1 Like

To be honest, I be more templated to control the progress of the animation direction for this

You can set the speed to 0 and update the current directly in update loop

1 Like

Yes, why not. Seems much more straightforward here. Thanks @yaustar, now things work just like I intended. Here is the final code I used, most importantly, updating and clamping activeStateCurrentTime manually:

// Update the current time and clamp it within the animation duration
this.currentTime += dt * speed;
this.currentTime = pc.math.clamp(this.currentTime, 0, this.entity.anim.baseLayer.activeStateDuration);
this.entity.anim.baseLayer.activeStateCurrentTime = this.currentTime;

// initialize code called once per entity
AnimationControllerTransition.prototype.initialize = function () {
    if (!this.entity.anim || !this.entity.anim.baseLayer) {
        console.error('Animation component or base layer is missing.');
        return;
    }

    this.entity.anim.speed = 0;
    this.currentTime = 0;
};

// update code called every frame
AnimationControllerTransition.prototype.update = function (dt) {
    if (this.app.keyboard.isPressed(this.forwardKey)) {
        this.playAnimation(dt, this.speedFactor, 'forwards');
    }

    if (this.app.keyboard.isPressed(this.backwardKey)) {
        this.playAnimation(dt, -this.speedFactor, 'backwards');
    }

    if (this.app.keyboard.wasReleased(this.forwardKey) || this.app.keyboard.wasReleased(this.backwardKey)) {
        this.pauseAnimation();
    }

};

// Play the animation in the specified direction
AnimationControllerTransition.prototype.playAnimation = function (dt, speed, direction) {
    if (!this.isPlaying) {
        this.isPlaying = true;
        console.log(`Playing ${direction} from ${this.currentTime.toFixed(2)}`);
    }

    // Update the current time and clamp it within the animation duration
    this.currentTime += dt * speed;
    this.currentTime = pc.math.clamp(this.currentTime, 0, this.entity.anim.baseLayer.activeStateDuration);
    this.entity.anim.baseLayer.activeStateCurrentTime = this.currentTime;
};

AnimationControllerTransition.prototype.pauseAnimation = function () {
    this.isPlaying = false;
    console.log(`Paused at ${this.currentTime.toFixed(2)} s`);
};

regards Björn

1 Like