Skip to content

Commit

Permalink
#164 fix animation tutorial (#177)
Browse files Browse the repository at this point in the history
* #164 Update the hello animation tutorial to use the modern AnimComposer
  • Loading branch information
richardTingle authored Oct 5, 2024
1 parent 1b43dc3 commit 5c917f2
Showing 1 changed file with 121 additions and 134 deletions.
255 changes: 121 additions & 134 deletions docs/modules/tutorials/pages/beginner/hello_animation.adoc
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
= jMonkeyEngine 3 Tutorial (7) - Hello Animation
:author:
:revnumber:
:revdate: 2020/07/06
:revdate: 2024/10/05
:keywords: beginner, intro, animation, documentation, keyinput, input, node, model


This tutorial shows how to add an animation controller and channels, and how to respond to user input by triggering an animation in a loaded model.
This tutorial shows how to add an animation controller and how to respond to user input by triggering an animation in a loaded model.

image::beginner/beginner-animation.png[beginner-animation.png,width="",height="",align="center"]

Expand All @@ -18,10 +18,12 @@ include::partial$add-testdata-tip.adoc[]
package jme3test.helloworld;
import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.animation.LoopMode;
import com.jme3.anim.AnimComposer;
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.BlendSpace;
import com.jme3.anim.tween.action.LinearBlendSpace;
import com.jme3.app.SimpleApplication;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
Expand All @@ -32,60 +34,71 @@ import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
/** Sample 7 - how to load an OgreXML model and play an animation,
* using channels, a controller, and an AnimEventListener. */
public class HelloAnimation extends SimpleApplication
implements AnimEventListener {
private AnimChannel channel;
private AnimControl control;
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.setLocalScale(0.5f);
rootNode.attachChild(player);
control = player.getControl(AnimControl.class);
control.addListener(this);
channel = control.createChannel();
channel.setAnim("stand");
}
public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
if (animName.equals("Walk")) {
channel.setAnim("stand", 0.50f);
channel.setLoopMode(LoopMode.DontLoop);
channel.setSpeed(1f);
* using AnimComposer */
public class HelloAnimation extends SimpleApplication{
private AnimComposer control;
private Action advance;
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.setLocalScale(0.5f);
rootNode.attachChild(player);
control = player.getControl(AnimComposer.class);
control.setCurrentAction("stand");
/* Compose an animation action named "halt"
that transitions from "Walk" to "stand" in half a second. */
BlendSpace quickBlend = new LinearBlendSpace(0f, 0.5f);
Action halt = control.actionBlended("halt", quickBlend, "stand", "Walk");
halt.setLength(0.5);
/* 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);
}
}
public void onAnimChange(AnimControl control, AnimChannel channel, 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 (!channel.getAnimationName().equals("Walk")) {
channel.setAnim("Walk", 0.50f);
channel.setLoopMode(LoopMode.Loop);
}
}
/**
* Callback to indicate that the "advance" animation action has completed.
*/
void onAdvanceDone() {
/*
* Play the "stand" animation action.
*/
control.setCurrentAction("stand");
}
/**
* Map the spacebar to the "Walk" input action, and add a listener to initiate
* the "advance" animation action each time it's pressed.
*/
private void initKeys() {
inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_SPACE));
ActionListener handler = new ActionListener() {
@Override
public void onAction(String name, boolean keyPressed, float tpf) {
if (keyPressed && control.getCurrentAction() != advance) {
control.setCurrentAction("advance");
}
}
};
inputManager.addListener(handler, "Walk");
}
};
}
----
Expand Down Expand Up @@ -128,59 +141,36 @@ After you load the animated model, you register it to the Animation Controller.
[source,java]
----
private AnimChannel channel;
private AnimControl control;
private AnimComposer control;
public void simpleInitApp() {
...
/* Load the animation controls, listen to animation events,
* create an animation channel, and bring the model in its default position.
*/
control = player.getControl(AnimControl.class);
control.addListener(this);
channel = control.createChannel();
channel.setAnim("stand");
control = player.getControl(AnimComposer.class);
control.setCurrentAction("stand");
...
}
----

This line of code will return NULL if the AnimControl is not in the main node of your model.
This line of code will return NULL if the AnimComposer is not in the main node of your model.

[source,java]
----
control = player.getControl(AnimControl.class);
control = player.getControl(AnimComposer.class);
----

To check this, btn:[RMB] select your model and click "`Edit in SceneComposer`" if the models file extension is .j3o, or "`View`" if not. You can then see the tree for the model so you can locate the node the control resides in. You can access the subnode with the following code.

[source,java]
----
player.getChild("Subnode").getControl(AnimControl.class);
player.getChild("Subnode").getControl(AnimComposer.class);
----

[NOTE]
====
In response to a question about animations on different channels interfering with each other, *Nehon*, on the jME forum wrote,
[quote, Nehon, Team Leader: Retired]
____
You have to consider channels as part of the skeleton that are animated. The default behavior is to use the whole skeleton for a channel.
In your example the first channel plays the walk anim, then the second channel plays the dodge animation.
Arms and feet are probably not affected by the doge animation so you can see the walk anim for them, but the rest of the body plays the dodge animation.
Usually multiple channels are used to animate different part of the body. For example you create one channel for the lower part of the body and one for the upper part. This allow you to play a walk animation with the lower part and for example a shoot animation with the upper part. This way your character can walk while shooting.
In your case, where you want animations to chain for the whole skeleton, you just have to use one channel.
____
====



== Responding to Animation Events

Add `implements AnimEventListener` to the class declaration. This interface gives you access to events that notify you when a sequence is done, or when you change from one sequence to another, so you can respond to it. In this example, you reset the character to a standing position after a `Walk` cycle is done.
A Tween (part of an action sequence) can call a method on a class, allowing your application code to be informed of the animation state. In this example, you reset the character to a standing position after a `Walk` cycle is done.

[source,java]
----
Expand All @@ -189,17 +179,24 @@ public class HelloAnimation extends SimpleApplication
implements AnimEventListener {
...
public void onAnimCycleDone(AnimControl control,
AnimChannel channel, String animName) {
if (animName.equals("Walk")) {
channel.setAnim("stand", 0.50f);
channel.setLoopMode(LoopMode.DontLoop);
channel.setSpeed(1f);
@Override
public void simpleInitApp() {
...
Action walk = control.action("Walk");
Tween doneTween = Tweens.callMethod(this, "onAdvanceDone");
advance = control.actionSequence("advance", walk, halt, doneTween);
...
}
/**
* Callback to indicate that the "advance" animation action has completed.
*/
void onAdvanceDone() {
/*
* Play the "stand" animation action.
*/
control.setCurrentAction("stand");
}
}
public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
// unused
}
...
}
----
Expand All @@ -220,45 +217,39 @@ There are ambient animations like animals or trees that you may want to trigger
[source,java]
----
private void initKeys() {
inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_SPACE));
inputManager.addListener(actionListener, "Walk");
}
----

To use the input controller, you need to implement the actionListener by testing for each action by name, then set the channel to the corresponding animation to run.

* The second parameter of setAnim() is the blendTime (how long the current animation should overlap with the last one).
* LoopMode can be Loop (repeat), Cycle (forward then backward), and DontLoop (only once).
* If needed, use channel.setSpeed() to set the speed of this animation.
* Optionally, use channel.setTime() to Fast-forward or rewind to a certain moment in time of this animation.

[source,java]
----
private ActionListener actionListener = new ActionListener() {
public void onAction(String name, boolean keyPressed, float tpf) {
if (name.equals("Walk") && !keyPressed) {
if (!channel.getAnimationName().equals("Walk")){
channel.setAnim("Walk", 0.50f);
channel.setLoopMode(LoopMode.Cycle);
/**
* Map the spacebar to the "Walk" input action, and add a listener to initiate
* the "advance" animation action each time it's pressed.
*/
private void initKeys() {
inputManager.addMapping("Walk", new KeyTrigger(KeyInput.KEY_SPACE));
ActionListener handler = new ActionListener() {
@Override
public void onAction(String name, boolean keyPressed, float tpf) {
if (keyPressed && control.getCurrentAction() != advance) {
control.setCurrentAction("advance");
}
}
}
};
inputManager.addListener(handler, "Walk");
}
};
----


* By default, the animation will loop, there is an overloaded `setCurrentAction` method that allows you to set the loop mode.
* If needed, use Action::setSpeed to set the speed of this animation.
* Optionally, use AnimComposer::.setTime to Fast-forward or rewind to a certain moment in time of this animation.

== Exercises


=== Exercise 1: Two Animations

Make a mouse click trigger another animation sequence!

. Create a second channel in the controller.
. Create a second layer in the controller.
. Create a new key trigger mapping and action. (see: xref:beginner/hello_input_system.adoc[Hello Input])
+
[TIP]
Expand All @@ -269,7 +260,7 @@ Use:
[source,java]
----
for (String anim : control.getAnimationNames()) {
for (String anim : control.getAnimClipsNames()) {
System.out.println(anim);
}
----
Expand All @@ -293,8 +284,8 @@ Add the following import statements for the SkeletonDebugger and Material classe
[source,java]
----
import com.jme3.scene.debug.SkeletonDebugger;
import com.jme3.material.Material;
import com.jme3.scene.debug.custom.ArmatureDebugAppState;
import com.jme3.anim.SkinningControl;
----

Expand All @@ -303,13 +294,9 @@ Add the following code snippet to `simpleInitApp()` to make the bones (that you
[source,java]
----
SkeletonDebugger skeletonDebug =
new SkeletonDebugger("skeleton", control.getSkeleton());
Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
mat.setColor("Color", ColorRGBA.Green);
mat.getAdditionalRenderState().setDepthTest(false);
skeletonDebug.setMaterial(mat);
player.attachChild(skeletonDebug);
ArmatureDebugAppState armatureDebugAppState = new ArmatureDebugAppState();
armatureDebugAppState.addArmatureFrom(player.getControl(SkinningControl.class));
this.getStateManager().attach(armatureDebugAppState);
----

Expand Down

0 comments on commit 5c917f2

Please sign in to comment.