Question about new Animation System part 2 - AudioTrack, EffectTrack and more

Hi everyone,
some time ago I wrote this help class for adding particle and sound effects to old system animations.

import com.jme3.anim.tween.Tween;
import com.jme3.animation.AnimControl;
import com.jme3.animation.Animation;
import com.jme3.animation.AudioTrack;
import com.jme3.animation.EffectTrack;
import com.jme3.audio.AudioNode;
import com.jme3.effect.ParticleEmitter;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.Control;

public class TrackUtils {

	public static void addAudioTrack(Spatial sp, AudioNode audio, String animName, float startOffset) {
		Animation anim = sp.getControl(AnimControl.class).getAnim(animName);
		AudioTrack track = new AudioTrack(audio, anim.getLength(), startOffset);
		anim.addTrack(track);
	}

	public static void addEffectTrack(Spatial sp, ParticleEmitter emitter, String animName, float startOffset) {
		Animation anim = sp.getControl(AnimControl.class).getAnim(animName);
		EffectTrack track = new EffectTrack(emitter, anim.getLength(), startOffset);
		anim.addTrack(track);
	}
	
}

Taking a cue from the AudioTrack and EffectTrack classes, I created a custom “CallbackTrack” class to be able to invoke a method via reflection at a precise moment in time in the animation. I needed this mechanism to be able to inflict damage on the player at a precise moment in the enemy attack animation.

Here is a sample snippet:

public class EnemyControl extends AbstractControl {

	private float damage = 5f;

    @Override
    public void setSpatial(Spatial sp) {
        super.setSpatial(sp);
        if (spatial != null) {
			
			Animation anim = spatial.getControl(AnimControl.class).getAnim("ZombieAttack");
			Tween tween = Tweens.callMethod(this, "inflictDamage", damage);
			float startOffset = 1f;
			
			CallbackTrack track = new CallbackTrack(tween, anim.getLength(), startOffset);
			anim.addTrack(track);
        }
    }
	
	public void inflictDamage(float damage) {
		// apply damage to the player...
	}
}

The CallbackTrack class:

package com.capdevon.anim;

import com.jme3.anim.tween.Tween;
import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.animation.Track;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.util.TempVars;
import com.jme3.util.clone.Cloner;
import com.jme3.util.clone.JmeCloneable;

/**
 *
 * @author capdevon
 */
public class CallbackTrack implements Track, JmeCloneable {
    
    private Tween tween;
    private float startOffset = 0;
    private float length = 0;
    private boolean initialized = false;
    private boolean started = false;

    //Animation listener to reset the tween when the animation ends or is changed
    private class OnEndListener implements AnimEventListener {

        @Override
        public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
            stop();
        }

        @Override
        public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
        }
    }

    /**
     * constructor for serialization only
     */
    protected CallbackTrack() {
    }

    /**
     * Creates an ActionTrack
     *
     * @param tween the Tween
     * @param length the length of the track (usually the length of the
     * animation you want to add the track to)
     */
    public CallbackTrack(Tween tween, float length) {
        this.tween = tween;
        this.length = length;
    }

    /**
     * Creates an ActionTrack
     *
     * @param tween the Tween
     * @param length the length of the track (usually the length of the
     * animation you want to add the track to)
     * @param startOffset the time in second when the tween will be played after
     * the animation starts (default is 0)
     */
    public CallbackTrack(Tween tween, float length, float startOffset) {
        this(tween, length);
        this.startOffset = startOffset;
    }

    /**
     * Internal use only
     *
     * @see Track#setTime(float, float, com.jme3.animation.AnimControl,
     * com.jme3.animation.AnimChannel, com.jme3.util.TempVars)
     */
    @Override
    public void setTime(float time, float weight, AnimControl control, AnimChannel channel, TempVars vars) {

        if (time >= length) {
            return;
        }
        if (!initialized) {
            control.addListener(new OnEndListener());
            initialized = true;
        }
        if (!started && time >= startOffset) {
            started = true;
            tween.interpolate(1);
        }
    }

    private void stop() {
        started = false;
    }

    /**
     * Return the length of the track
     *
     * @return length of the track
     */
    @Override
    public float getLength() {
        return length;
    }

    @Override
    public float[] getKeyFrameTimes() {
        return new float[] { startOffset };
    }
    
    /**
     * Clone this track
     *
     * @return a new track
     */
    @Override
    public Track clone() {
        return new CallbackTrack(tween, length, startOffset);
    }

    @Override   
    public Object jmeClone() {
        try {
            return super.clone();
        } catch( CloneNotSupportedException e ) {
            throw new RuntimeException("Error cloning", e);
        }
    }     

    @Override   
    public void cloneFields( Cloner cloner, Object original ) {
        // Duplicating the old cloned state from cloneForSpatial()
        this.initialized = false;
        this.started = false;
        this.tween = cloner.clone(tween);
    }

    @Override
    public void write(JmeExporter ex) throws IOException {
        throw new UnsupportedOperationException("Not supported yet.");
    }

    @Override
    public void read(JmeImporter im) throws IOException {
        throw new UnsupportedOperationException("Not supported yet.");
    }
    
}

.
How can I translate the same features with the new animation system?

Without drilling in too deeply (distracted at the moment), the new system is much more flexible and already has callback support. So you could construct a sequence of delay + callback “out of the box”, I think.

1 Like

Something like that ?

public class EnemyControl extends AbstractControl {

	private float damage = 5f;

    @Override
    public void setSpatial(Spatial sp) {
        super.setSpatial(sp);
        if (spatial != null) {
			
			// Animation anim = spatial.getControl(AnimControl.class).getAnim("ZombieAttack");
			// Tween tween = Tweens.callMethod(this, "inflictDamage", damage);
			float startOffset = 1f;
			// 
			// CallbackTrack track = new CallbackTrack(tween, anim.getLength(), startOffset);
			// anim.addTrack(track);
			
			AnimComposer animComposer = spatial.getControl(AnimComposer.class);
			AnimClip animClip = animComposer.getAnimClip("ZombieAttack");
			Action action = animComposer.action(animClip.getName());
			Tween sequence = Tweens.sequence(action, Tweens.delay(startOffset), Tweens.callMethod(this, "inflictDamage", damage));
			
			action = new BaseAction(sequence);
			animComposer.addAction(animClip.getName(), action);
        }
    }
	
	public void inflictDamage(float damage) {
		// apply damage to the player...
	}
}

I think so. (I’m up to my eyeballs in day job related BS so I’m only visiting here.)

Any suggestions about it? could you tell me if my example is correct? If so, is it right to use Tween.sequence in combination with Tween.callMethod also to perform audio or particle effects?

Something like that:

public class EnemyControl extends AbstractControl {

	private AudioNode screamSFX;

    @Override
    public void setSpatial(Spatial sp) {
        super.setSpatial(sp);
        if (spatial != null) {
			AnimComposer animComposer = spatial.getControl(AnimComposer.class);
			AnimClip animClip = animComposer.getAnimClip("ZombieScream");
			Action action = animComposer.action(animClip.getName());
			Tween sequence = Tweens.sequence(action, Tweens.callMethod(this, "scream"));
			
			action = new BaseAction(sequence);
			animComposer.addAction(animClip.getName(), action);
        }
    }
	
	public void scream() {
		screamSFX.playInstance();
	}
}

Yes, this an advantage of Tween API that lets you chain them together.

2 Likes

ok, thanks :wink:

1 Like