[Solved] Bug using sequenced actions with new anim system

I spent some time trying to use tweens to call a stopAnimation() method when an action is done, but it seems to cause all layers of the animation rig to bug out and pause half way through playing the next animation, even when the stopAnimation() method is blank.

So either something is wrong with tweens ,or my code below is wrong.

Here was the code I tried using for a non-loop mode that I thought should work.

           if(!loopMode.equals(LoopMode.Loop)){ ///non looping
                Action tempAction = animComposer.action(currentAnimation);
                Tween doneTween = Tweens.callMethod(this, "stopCurrentAnim");
                singleAction = animComposer.actionSequence(currentAnimation + "_nonLooping", tempAction, doneTween);
                
                animComposer.setCurrentAction(currentAnimation + "_nonLooping", layerName);
            }else{
                animComposer.setCurrentAction(currentAnimation, layerName);                
            }

And here is the stop animation method it should call when done.

    public void stopCurrentAnim(){
        
//        if(armatureMask != null){
//            animComposer.removeCurrentAction(layerName);
//        }
//        
//        currentAnimation = null;
    }

I commented out all the code so this method does nothing, but still I get interruptions in the following animations when I use tweens to call the method when an action is done, even on layers that were running an entirely different looping animation the whole time

I’m personally not very invested in solving this because I got things to work without tweens (using timers instead) and the bugginess goes away. But I might use tweens for other stuff in the future so I thought I should at least report this to see if its something I’m doing wrong, or if its an issue with using tweens to call a method after an action.

I have a strong suspicion that there is more going on than what’s been shown.

Just in case, to confirm… if the ONLY Thing you change is removing the “, doneTween” in the below…

Then things stop glitching?

Or does the “doesn’t do anything” doneTween actually have nothing to do with what you are seeing?

There’s just not a lot of magic here that could be causing strange things. So that’s why I think the issue must be elsewhere.

1 Like

Here is how I was changing my code to avoid the glitching, I just commented out all of the tween-related code:

    public void setAnim(String animName) {
        if(hasLayer(animComposer, layerName)){
            currentAnimation = animName;
            
//            if(!loopMode.equals(LoopMode.Loop)){ ///non looping
//                Action tempAction = animComposer.action(currentAnimation);
//                Tween doneTween = Tweens.callMethod(this, "stopCurrentAnim");
//                singleAction = animComposer.actionSequence(currentAnimation + "_nonLooping", tempAction, doneTween);
//                
//                animComposer.setCurrentAction(currentAnimation + "_nonLooping", layerName);
//            }else{
                animComposer.setCurrentAction(currentAnimation, layerName);                
//            }
        }
    }

But as suggested by your reply, I just tried changing the code I originally posted to remove the doneTween from the sequence

Action tempAction = animComposer.action(currentAnimation);
singleAction = animComposer.actionSequence(currentAnimation + "_nonLooping", tempAction);
                
animComposer.setCurrentAction(currentAnimation + "_nonLooping", layerName);

And the glitching persists. So you’re right the doneTween wasn’t casuing it, but instead it seems to be the call to animComposer.actionSequence() or creating the tempAction.

I’ve encountered this error many more time since I made this post, the bug consistently appears when an action that was created with the animComposer.actionSequence() method is played on a single ArmatureMask, and it cause bones on every other ArmatureMasks to twitch momentarily.

If I use the sequenced Action to play an animation on the full AnimComposer, the bug doesn’t seem to happen, so it seems exclusive to setups with multiple armatureMasks.

Does anyone else have any experience trying to play an Action created with the actionSequence() method on one armatureMask while another armatureMask is playing a different animation?

Can you provide a minimal test case?

Yes I will work on putting one together and post it as soon as I can.

1 Like

I was able to put it together quicker than I expected, and it reproduces the error exactly the same as in my game. I split it into 2 short classes but it is still very simple.

I was going to just post the code in a forum post here, but I also needed to upload my model so I just made a quick github repo, and uploaded 2 java classes and a single Elf model in the models folder

When the test case starts up, the model is just playing a walk animation, and if you press the “N” key then it will play the swing animation once on the upper body, and this is when you can see it also interrupts the walk animation on the lower body for some reason.

It becomes even more noticeable if you press “n” rapidly.

1 Like

This test case also shows another bug I’ve been encountering, coincidentally:

Once the swing is done playing, the upper body goes back to playing the walk animation, but it is out of sync with the walk animation that’s been playing on the lower body. I tried calling the .setTime() method on the upper body’s walk animation to make it match the time of the lower body walk animation, but doing so breaks the smooth transition from swinging to walking…

I could’ve swore I made another thread about that issue because I have a video I made that shows that bug on my imgur posts, but I will hold off on discussing that issue until I find that thread or make a new thread.

1 Like

You are running into this issue:

Simply put, your mask is ignored on the sequencedAction.

Action sequencedAction = animComposer.actionSequence("sequencedAction", singleAction, doneTween);

because the parent does not propagate the mask to its internal actions which actually need the mask (singleAction in your example).

We need to either modify the Action class to propagate the mask to its internal actions (but then child actions can not have their own different mask, but do they need?) or you need to specifically set the mask on the child actions. So in your example, it should be:

Action singleAction = animComposer.action("heavySwing");
singleAction.setMask(upperBodyMask);

I do not know the original author’s opinion but IMO we should take the first approach (i.e. parent action should propagate mask to its child actions). Not sure though!

@sgold, @pspeed what do you think?

3 Likes

But if we propagate then how would we have a sequence with different masks? Can anyone think of a use-case for that? (And certainly we would not want to do it for parallel actions and maybe we should be consistent?)

Perhaps some utility-style methods that would propagate it would be a compromise.

2 Likes

what do you think?

I don’t have an opinion on this matter.

1 Like

Yeah, though we may need to make the actions list public for that as it is protected currently or one can simply extend BaseAction class and override the setMask() method and there propagate it to children. Something like this:

public class PropagatedMaskAction extends BaseAction {

     public void setMask(AnimationMask mask) { 
         this.mask = mask;

         for(Action action : actions) {
            action.setMask(mask);
         } 
     } 

}
2 Likes

Or you can instead reset the lower body animation so both will start from time 0 with a smooth transition applied.

    public void stopUpperBodyAnimation() {

        animComposer.setCurrentAction("ladyWalk", "upperBody");
        animComposer.setCurrentAction("ladyWalk", "lowerBody");

    }

Hi guys,
I read the topic and your ideas with a lot of interest.

I cleaned up some code I had written during my studies when I faced the same problem. Here is a test case that might be useful. It is generic and configurable and could offer other interesting food for thought.

It uses the super Heart library, by @sgold, to work. I used the SkeletonVisualizer to highlight the animation layers.

I used 2 animation layers:

  • the default one which includes the whole skeleton (red potins).
  • the other includes only the upper body (green points).

When the upper animation (which overrides the layer below) ends, the synchronization with the lower body happens automatically, without having to set the timer.

package com.test.main;

import com.jme3.anim.AnimComposer;
import com.jme3.anim.AnimationMask;
import com.jme3.anim.ArmatureMask;
import com.jme3.anim.Joint;
import com.jme3.anim.SkinningControl;
import com.jme3.anim.tween.Tween;
import com.jme3.anim.tween.Tweens;
import com.jme3.anim.tween.action.Action;
import com.jme3.anim.tween.action.BaseAction;
import com.jme3.app.SimpleApplication;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.Trigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.material.Materials;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.post.FilterPostProcessor;
import com.jme3.post.filters.FXAAFilter;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;
import com.jme3.scene.shape.Quad;
import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.system.AppSettings;

import jme3utilities.debug.SkeletonVisualizer;

/**
 * 
 * @author capdevon
 */
public class Test_AnimLayers extends SimpleApplication implements ActionListener {

    /**
     * @param args
     */
    public static void main(String[] args) {
        Test_AnimLayers app = new Test_AnimLayers();
        AppSettings settings = new AppSettings(true);
        settings.setResolution(1024, 768);
        settings.setFrameRate(60);
        settings.setBitsPerPixel(24);
        app.setSettings(settings);
        app.setShowSettings(false);
        app.setPauseOnLostFocus(false);
        app.start();
    }

    private AnimComposer animComposer;
    private SkinningControl skinningControl;
    private final String upperBodyLayer = "upperBody";

    // <replace assetModel with yours>
    private final String assetModel = "Models/gltf2/RiflePack/character_1.gltf";
    private final String spineJoint = "Armature_mixamorig:Spine";
    
    // <define your animations here>
    private final AnimDef reloadAnim = new AnimDef("rifle-reloading", false, upperBodyLayer);
    private final AnimDef walkAnim = new AnimDef("walk-with-rifle", true);

    @Override
    public void simpleInitApp() {

        addLighting();
        createFloor();
        setupCharacter();
        configureCamera();
        setupKeys();
    }

    private void configureCamera() {
        flyCam.setDragToRotate(true);
        flyCam.setMoveSpeed(20);
        cam.setLocation(new Vector3f(0, 2, 4));
    }

    private void addLighting() {
        // Set the viewport's background color to light blue.
        ColorRGBA skyColor = new ColorRGBA(0.1f, 0.2f, 0.4f, 1f);
        viewPort.setBackgroundColor(skyColor);
        
        rootNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);

        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-0.2f, -1, -0.3f).normalizeLocal());
        sun.setName("Sun");
        rootNode.addLight(sun);

        AmbientLight ambient = new AmbientLight();
        ambient.setColor(new ColorRGBA(0.25f, 0.25f, 0.25f, 1));
        ambient.setName("Ambient");
        rootNode.addLight(ambient);
        
        DirectionalLightShadowFilter dlsf = new DirectionalLightShadowFilter(assetManager, 4096, 3);
        dlsf.setLight(sun);
        dlsf.setShadowIntensity(0.4f);
        dlsf.setShadowZExtend(256);
        
        FXAAFilter fxaa = new FXAAFilter();
        
        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
        fpp.addFilter(dlsf);
        fpp.addFilter(fxaa);
        viewPort.addProcessor(fpp);
    }

    private void createFloor() {
        Quad quad = new Quad(10, 10);
        quad.scaleTextureCoordinates(new Vector2f(5, 5));
        Geometry floor = new Geometry("Stage", quad);
        Material mat = new Material(assetManager, Materials.UNSHADED);
        mat.setColor("Color", ColorRGBA.Gray);
        floor.setMaterial(mat);
        floor.rotate(-FastMath.HALF_PI, 0, 0);
        floor.center();
        rootNode.attachChild(floor);
    }

    private void setupCharacter() {
    	Node model = (Node) assetManager.loadModel(assetModel);
        rootNode.attachChild(model);

        animComposer = findControl(model, AnimComposer.class);
        skinningControl = findControl(model, SkinningControl.class);

        ArmatureMask upperBodyMask = new ArmatureMask();
        upperBodyMask.addFromJoint(skinningControl.getArmature(), spineJoint);
        animComposer.makeLayer(upperBodyLayer, upperBodyMask);
        
        SkeletonVisualizer sv = createSkeletonDebug(skinningControl, upperBodyMask);
        model.addControl(sv);
        sv.setEnabled(true);

        createAnimCallback(reloadAnim, upperBodyMask);
        playAnimation(walkAnim);
    }

    private SkeletonVisualizer createSkeletonDebug(SkinningControl sc, AnimationMask mask) {
        SkeletonVisualizer sv = new SkeletonVisualizer(assetManager, sc);
        sv.setHeadSize(8);

        for (Joint joint : sc.getArmature().getJointList()) {
            boolean contains = mask.contains(joint);
            sv.setHeadColor(joint.getId(), contains ? ColorRGBA.Green : ColorRGBA.Red);
        }
        
        return sv;
    }

    private void createAnimCallback(AnimDef def, AnimationMask mask) {
        // Get action registered with specified name. It will make a new action if there isn't any.
        Action action = animComposer.action(def.animName);
        action.setMask(mask);
        Tween callback = Tweens.callMethod(this, "animCycleDone", def);

        // Register custom action with specified name.
        action = new BaseAction(Tweens.sequence(action, callback));
        animComposer.addAction(def.animName, action);
    }

    private void setupKeys() {
        addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_SPACE));
    }

    private void addMapping(String mappingName, Trigger... triggers) {
        inputManager.addMapping(mappingName, triggers);
        inputManager.addListener(this, mappingName);
    }

    @Override
    public void onAction(String name, boolean isPressed, float tpf) {
        if (name.equals("nextAnim") && isPressed) {
        	playAnimation(reloadAnim);
        }
    }
    
    void playAnimation(AnimDef def) {
    	animComposer.setCurrentAction(def.animName, def.layerName);
    }

    void animCycleDone(AnimDef def) {
        if (!def.loop) {
            animComposer.removeCurrentAction(def.layerName);
        }
    }

    private <T extends Control> T findControl(Spatial sp, Class<T> clazz) {
        T control = sp.getControl(clazz);
        if (control != null) {
            return control;
        }
        if (sp instanceof Node) {
            for (Spatial child : ((Node) sp).getChildren()) {
                control = findControl(child, clazz);
                if (control != null) {
                    return control;
                }
            }
        }
        return null;
    }

    public class AnimDef {

        public final String animName;
        public final String layerName;
        public final boolean loop;

        public AnimDef(String name, boolean loop) {
            this(name, loop, AnimComposer.DEFAULT_LAYER);
        }

        public AnimDef(String name, boolean loop, String layerName) {
            this.animName = name;
            this.layerName = layerName;
            this.loop = loop;
        }

    }

}

The new animation system is very flexible and perhaps does not want to be rigid just to allow maximum customization. It looks like a raw module that needs to be modeled with an additional level based on your gaming needs.

2 Likes

In general, I’m a fan of getters and more introspectability. I’m not a fan of making fields public. That’s API-death.

Edit: and to follow up, I find the new animation system frustratingly opaque. I’ve hesitated to do anything about it only because I don’t know if there was a good reason for some of it or just “didn’t get to it yet” from the original author. But I would not object to more getters.

4 Likes

Unfortunately I do not think this will work with the way im using DynamicAnimControl.

There are times where the upper body will need to be in dynamic mode being controlled by DAC while the lower body is walking (for example, when a raised shield gets hit hard and upper body flails backwards with a dynamic-force from DAC while the lower body plays a back-walk animation)

If the upper body is also part of the lowerBody mask, then you end up with something buggy like this:

https://www.youtube.com/shorts/7GXg8U3uOa4
(dumb youtube made this a short and isnt showing a preview lol…)

So in order for things to work here, the lowerBody mask cannot share any bones with the upperBody mask so that the upperBody’s bones can have all animations removed when DAC needs to take over.

I also do not think this would work without causing a noticeable flinch in the lowerBody every time it is reset to 0 frame while in the middle of walking.

Maybe the best solution for this issue with the upper and lower body being out of sync is to write some code that causes the upperBody to play the walk animation at a lower speed until its time matches the time of the lowerBody.


I unfortunately don’t have much useful input about the first bug regarding the Action not running properly on a single mask since I’m only just starting to study the new anim system’s source code. But I appreciate the help trying to get to the bottom of this issue, and hopefully we will find the optimal solution! :slightly_smiling_face:

3 Likes

Yes, I mean a public getter.

This does fix the issue in my test case, but that test case only uses 2 armatureMasks and doesn’t try to do anything more advanced.

Unfortunately, adding the Action.setMask() call into my game breaks everything. Now, if I try playing an Animation on multiple masks at once it will only cause the animation to run on just a single layer, likely because that is the most recent one I called action.setArmatureMask(mask) for, and it overwrites the armatureMask variable in the Action class.

I am still investigating the source code so I could be wrong on my assumptions here:

But my firs thought is that It feels like a flawed design to to store a reference to an ArmatureMask in the Action class, as it implies that a single action can only be played on one armatureMask at a time.

If the Action class absolutely needs to keep a reference to the currently affected ArmatureMasks, then I would suggest to instead store a list of ArmatureMasks in the Action class and that would probably solve the issue I’m now encountering, and would allow an Action to be played on more than one ArmatureMask at once.

But this raises the question as to why I didn’t have issues before I decided to manually call setArmatureMask() prior to playing an action myself. I have always been playing a single action on multiple layers at the same time before this with no trouble.

When I look at the source code, it appears the update loop in an AnimLayer sets the currentAction’s mask to that layer’s mask, calls a single line of code, and then set it to null. :

So I’m guessing that action.setArmatureMask() is a method that is only meant to be called internally? It appears that the AnimLayer class is the only place this method gets called from in the source code.

Any thoughts on this?

Edit: here is also the only class that calls the getMask() method, so I think if we do decide to make it support a list of armatureMasks, it would need updated here as well:

1 Like

You can use the PropagatedMaskAction I explained above it will solve the issue.

It will work just fine in single ClipAction because the layer will set the mask directly on it, issue will show up when you make a sequence action because it will wrap the single actions inside another action (BaseAction). And when the layer set the mask, the parent (BaseAction) will never forward it back to the single actions. That is why I suggested using a PropagatedMaskAction.

Edit:

Though maybe you want to use a better naming like MaskedAction or BranchedMaskAction or something :slight_smile:

1 Like

I will give that a try, although so far I’m struggling to understand exactly how I should create this new type of action.

I tried this code, however the results are the same:

                Action actionToPlay = animComposer.action(currentAnimation);
                actionToPlay.setMask(armatureMask);
                
                Tween doneTween = Tweens.callMethod(this, "stopCurrentAnim");
                Action sequencedAction = animComposer.actionSequence(currentAnimation + "_nonLooping_Sequenced", actionToPlay, doneTween);
                
                PropagatedMaskAction propagatedMaskAction = new PropagatedMaskAction(sequencedAction);
                animComposer.addAction(currentAnimation + "_nonLooping", propagatedMaskAction);
                

                animComposer.setCurrentAction(currentAnimation + "_nonLooping", layerName);

Is there a different way I should do this? The PropogatedAction constructor takes in a singleTween object, so I still have to call actionSequence to get a version of the Action that plays the Action followed by the done Tween.

What would be the proper way to use this here?