HelloAnimation tutorial for new Anim System

Since I’m deep into converting my game to the new system and its fresh in my head, I decided to update the HelloAnimation tutorial with a new version using AnimComposer and such.

I am unsure if I can be the one (or should be) to add additional text explaining everything thoroughly, but hopefully this will serve as a good starting point for replacing all of the old documentation eventually, and in the mean-time this can at least serve as a test-case / example that we can direct new users towards when they are trying to look for info on the new system

However there are 3 small things that I am not sure how to do in the new system that need fixed in this test case still:

  1. How to compare the name of an Action thats being run on a specific layer? For example, the animCoposer.getCurrentAction(layerName) method returns an Action object, but there is no way to get the name for comparing to an animation’s name, which the hello animation tutorial usually attempts to do in the onAction method before setting the animation to “walk”. Any ideas on the proper way to compare a layer’s current Action to its String based name?

  2. I can’t seem to find a direct replacement for AnimEventListener with the new system. Does one exist, or is it not necessary with the new anim system? (I personally don’t use listeners to check when anims are done so I’m unsure about this)

  3. Also not entirely sure how to handle loop mode with the new system. I personally use timers for animations that aren’t supposed to loop so that another looping walk/stand anim resume when the timer is up, but IDK what the optimal way to do non-looping is with the new system.

import com.jme3.anim.AnimComposer;
import com.jme3.anim.Armature;
import com.jme3.anim.ArmatureMask;
import com.jme3.anim.SkinningControl;
import com.jme3.anim.util.AnimMigrationUtils;
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.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;

/** Sample 7 - how to load an OgreXML model and play an animation,
 * using armatureMasks, an animComposer, a skinningControl, an armature, and an (insert new version of AnimEventListener  here if exists) */

public class HelloAnimation extends SimpleApplication  implements AnimEventListener {
    private ArmatureMask armatureMask;
    private AnimComposer animComposer;
    private SkinningControl skinningControl;
    private Armature armature;
  
    Node player;
    public static void main(String[] args) {
        HelloAnimation app = new HelloAnimation();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        viewPort.setBackgroundColor(ColorRGBA.LightGray);
        initKeys();
        DirectionalLight dl = new DirectionalLight();
        dl.setDirection(new Vector3f(-0.1f, -1f, -1).normalizeLocal());
        rootNode.addLight(dl);
        player = (Node) assetManager.loadModel("Models/Oto/Oto.mesh.xml");


        player = (Node) AnimMigrationUtils.migrate(player);        //convert the model to the new animation system, if necessary. This is only necessary if your model has an AnimControl and SkeletonControl instead of an AnimComposer and SkinningControl

        player.setLocalScale(0.5f);
        rootNode.attachChild(player);
        animComposer = player.getControl(AnimComposer.class);
        skinningControl = player.getControl(SkinningControl.class);



        armature = skinningControl.getArmature();

     //   composer.addListener(this); //does an equivelent exist in new system? (question 2)
     
         armatureMask = new ArmatureMask(armature); //passing armature into constructor will add all of the Armature's joints to the mask
       // armatureMask.addBones(armature, jointNames); //alternate way to add specific joints
       // armatureMask.addFromJoint(armature, jointName); //alternate way to add all joints descending from a designated parent joint

        animComposer.makeLayer("rootLayerMask", armatureMask);

        animComposer.setCurrentAction("stand", "rootLayerMask");
   }
 
    public void onAnimCycleDone(AnimComposer composer, ArmatureMask mask, String animName) {
         if (animName.equals("Walk")) {
           animComposer.setCurrentAction("stand", "rootLayerMask");
           //  channel.setLoopMode(LoopMode.DontLoop);  // how do non-looping now? ( question 3)
           animComposer.getCurrentAction("rootLayerMask").setSpeed(1.0f);

        }
    }

    public void onAnimChange(AnimComposer composer, ArmatureMask mask, String animName) {
    // unused
    }

  /** Custom Keybinding: Map named actions to inputs. */
    private void initKeys() {
        inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_SPACE));
        inputManager.addListener(actionListener, "Walk");
    }
    
    
    private ActionListener actionListener = new ActionListener() {
        public void onAction(String name, boolean keyPressed, float tpf) {
            if (name.equals("Walk") && !keyPressed) {
                if (!animComposer.getCurrentAction("rootLayerMask").equals("Walk")) {  // how to properly compare a layer's current action to know if its already playing? (question 1)
                    animComposer.setCurrentAction("stand", "rootLayerMask");
                    animComposer.getCurrentAction("rootLayerMask").setSpeed(0.5f);
           //   channel.setLoopMode(LoopMode.Loop);
                }
            }
        } 
  };
}
5 Likes

So i just realized an example already existed although i didnt find it on my first look somehow.

I think this example will answer most of my questions i had.

If this example is considered sufficient then maybe it could be a good idea to put it in the official wiki now so its easy to find

3 Likes

hi there donuts! thank you very much for starting on this. I am new to java and jmonkey and animation has so far been the most challenging aspect of what I’ve been teaching myself compounded by the confusion of old vs. new system.

Once I felt like I had a good handle on this I was going to try to update the wiki myself as it’s so useful and the animation system part being touched up would really be amazing I think. You are farther along than I am and I am pretty pumped!

I found this the other day too which I hadn’t found on previous search throughs but I don’t really know how to get around github well just yet. Anywho that on the wiki would be pretty fine I think as well

1 Like

So what do you think is the best way to loop/not loop?

The looping section here Animation in jME3 has this bit of code

Action walk = animComposer.action("Walk");
  Tween doneTween = Tweens.callMethod(animComposer, "setCurrentAction", "Idle");
  Action walkOnce = animComposer.actionSequence("WalkOnce", walk, doneTween);
  animComposer.setCurrentAction("WalkOnce");

I was able to get that ^ working with a walk and idle animation. On the github HelloAnimation we have

/* Compose an animation action named "advance"
       that walks for one cycle, then halts, then invokes onAdvanceDone(). */
Action walk = control.action("Walk");
    Tween doneTween = Tweens.callMethod(this, "onAdvanceDone");
    advance = control.actionSequence("advance", walk, halt, doneTween);

Anywho is this what you figured out was most optimal for looping? In my head I sure like the idea of LoopMode.DontLoop better than all these tweenz but that might just be the way the :cookie: crumbles

2 Likes

I haven’t implemented any code using Tweens this way yet, and am not sure still if that’s the best way for achieving a non-loop mode, but I could be wrong and am open to hear a differing opinion if anyone thinks strongly otherwise.

The first thing that I wonder is what would happen if the animation is cancelled before finishing. With just the code shown in the examples we’re referencing, it appears that it would still require more code to return to idle if a full walk animation cycle isn’t complete.

It also seems like it would not work in my case to always assume that the animation to play on finish is the same. For example, at the end of playing a swing animation, the next animation to play is different depending on whether the caster is standing, walking, sprinting, swimming/treading in water, in mid air, etc etc. My code in the update loop already handles which base animation state to loop in for every channel by checking the Agent’s current state, so adding redundant code to do the same thing in an onFinished() method is entirely unnecessary for a well written animation-state manager (in my opinion).

Even with the old animation system, I didn’t use the event system to listen for when an animation was done. Instead I set a timer for each animChannel (or armatureMask in the new system) equal to the duration of the animation that is playing on that channel. Then when the timer runs out, some other code in the update loop kicks in to make that channel play its default idle, walk, or run animation depending on the player’s last movement vector, and i they’re on land, in water, or in the air. There’s also no need to worry about what happens when an animation is cancelled (just set timer to 0) or another non-looping animation is played over it (in which case the timer is set to match the new animation)

So I will probably stick with my way of using timers instead of tweens in this case. I see some other good use cases for tweens, but in this case, managing the base animation state in the update loop while setting a timer for each mask’s layer when a non-looping animation is playing on that layer seems like its much cleaner and less code.

1 Like

I am not the one who can help you in migrating process.

But, i was experimenting with the new animation system recently while i was working on the docs PR (i am still there but trying to find good time :slight_smile: ).

Those are some basic testcases that expose the new animation system core functionalities incorporated into states including :

  1. Basic AnimComposer control.
  2. Basic Blender import.
  3. Basic TransformTrack.
  4. Basic Animation layers.
  5. Basic Tweens.
  6. Animation pause/resume.
  7. Custom Blendspaces (Radial and piechart blendspaces).
  8. Animation frame listener using Tweens.callTweenMethod (you can break the animation into several chunks and do a variety of things based on the current time, it just embeds a reflective call to your method passing in the current time).

Suggestions for your questions :

If the class Action doesn’t provide a getName then you can simply create some reference Action objects with same names in a HashMap and still be able to compare them.

Alright, for me, this was vague too, until i found the Tweens, Tweens are able to inject actions into your animation interpolate() loop, so you have multiple options including using Tweens#callTweenMethod() or else extend BaseAction and override doInterpolate and play around with the same way callTweenMethod do, you can listen to animation time stamps.

This is an example of AudioListener using the new system (EmitterTween in the repo) :

    private void onInterpolation(final float time, final String args){
        //TODO - do something when called by a parallel tween - this method would be called each interpolation - till we reach shockThunder.getAudioData().getDuration().
        //TODO - check for the timings
        if(time >= shockThunder.getAudioData().getDuration()) {
            //TODO - listener to the end of the track
            System.out.println(args + " " + time + " Reached the full time");
        }else if(time <= shockThunder.getAudioData().getDuration() / 4f){
            //TODO - listener to the middle of the track
            System.out.println(args + " " + time + " stills less than quarter the time");
        }else if(time <= shockThunder.getAudioData().getDuration() / 2f){
            //TODO - listener to the quarter of the track
            System.out.println(args + " " + time + " Reached half the time");
        }
    }

To call :

final BaseAction shockMyScreen = new BaseAction(Tweens.parallel(Tweens.callTweenMethod(shockThunder.getAudioData().getDuration(), this, "onInterpolation", "Hi from tween, after the sound finishes"), clipAction));
animComposer.addAction("ShockMyScreen", shockMyScreen);
//make a new Animation Layer that would hold a one current running action.....
animComposer.makeLayer(LayerBuilder.LAYER_EMITTER_TWEEN, new ArmatureMask());

action.setSpeed(0) or animComposer.removeCurrentAction(LayerBuilder.LAYER_BASIC_TWEEN); can also stop the animation, you can do action.setSpeed(0) to stop the animation when it reaches its mature and chain a call to another animation (or build a chainable state in which you can each time input the current transformations of the object).

EDIT :
For pausing/resuming the animation, you will need to add an extra step involving re-adding the animation again to start from the current transformation.

So, in your animation state :
Pause → Remove Action.
Resume → Recreate and run the new Action (starting with the new local transformation).

2 Likes

I like your idea of using timers I will dfeinitely experiment with that-

and this also kind of sounds like what I would want to be doing for my characters as well

1 Like

thanks for the post and examples pavl a lot of good concepts for me to mess around with
:male_detective:

1 Like