Răsfoiți Sursa

Trying to make Cinematic serialization work.
This way at least it doesn't crash: Removed anonymous inner class and made new class; CameraEvent
Added a bunch of default constructors for other related classes in the process.

Rickard Edén 7 ani în urmă
părinte
comite
ae97614c83

+ 369 - 365
jme3-core/src/main/java/com/jme3/animation/AnimChannel.java

@@ -1,365 +1,369 @@
-/*
- * Copyright (c) 2009-2012 jMonkeyEngine
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * * Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- *
- * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
- *   may be used to endorse or promote products derived from this software
- *   without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.jme3.animation;
-
-import com.jme3.math.FastMath;
-import com.jme3.util.TempVars;
-import java.util.BitSet;
-
-/**
- * <code>AnimChannel</code> provides controls, such as play, pause,
- * fast forward, etc, for an animation. The animation
- * channel may influence the entire model or specific bones of the model's
- * skeleton. A single model may have multiple animation channels influencing
- * various parts of its body. For example, a character model may have an
- * animation channel for its feet, and another for its torso, and
- * the animations for each channel are controlled independently.
- * 
- * @author Kirill Vainer
- */
-public final class AnimChannel {
-
-    private static final float DEFAULT_BLEND_TIME = 0.15f;
-    
-    private AnimControl control;
-
-    private BitSet affectedBones;
-
-    private Animation animation;
-    private Animation blendFrom;
-    private float time;
-    private float speed;
-    private float timeBlendFrom;
-    private float blendTime;
-    private float speedBlendFrom;
-    private boolean notified=false;
-
-    private LoopMode loopMode, loopModeBlendFrom;
-    
-    private float blendAmount = 1f;
-    private float blendRate   = 0;
-    
-    AnimChannel(AnimControl control){
-        this.control = control;
-    }
-
-    /**
-     * Returns the parent control of this AnimChannel.
-     * 
-     * @return the parent control of this AnimChannel.
-     * @see AnimControl
-     */
-    public AnimControl getControl() {
-        return control;
-    }
-    
-    /**
-     * @return The name of the currently playing animation, or null if
-     * none is assigned.
-     *
-     * @see AnimChannel#setAnim(java.lang.String) 
-     */
-    public String getAnimationName() {
-        return animation != null ? animation.getName() : null;
-    }
-
-    /**
-     * @return The loop mode currently set for the animation. The loop mode
-     * determines what will happen to the animation once it finishes
-     * playing.
-     * 
-     * For more information, see the LoopMode enum class.
-     * @see LoopMode
-     * @see AnimChannel#setLoopMode(com.jme3.animation.LoopMode)
-     */
-    public LoopMode getLoopMode() {
-        return loopMode;
-    }
-
-    /**
-     * @param loopMode Set the loop mode for the channel. The loop mode
-     * determines what will happen to the animation once it finishes
-     * playing.
-     *
-     * For more information, see the LoopMode enum class.
-     * @see LoopMode
-     */
-    public void setLoopMode(LoopMode loopMode) {
-        this.loopMode = loopMode;
-    }
-
-    /**
-     * @return The speed that is assigned to the animation channel. The speed
-     * is a scale value starting from 0.0, at 1.0 the animation will play
-     * at its default speed.
-     *
-     * @see AnimChannel#setSpeed(float)
-     */
-    public float getSpeed() {
-        return speed;
-    }
-
-    /**
-     * @param speed Set the speed of the animation channel. The speed
-     * is a scale value starting from 0.0, at 1.0 the animation will play
-     * at its default speed.
-     */
-    public void setSpeed(float speed) {
-        this.speed = speed;
-        if(blendTime>0){
-            this.speedBlendFrom = speed;
-            blendTime = Math.min(blendTime, animation.getLength() / speed);  
-            blendRate = 1/ blendTime;
-        }
-    }
-
-    /**
-     * @return The time of the currently playing animation. The time
-     * starts at 0 and continues on until getAnimMaxTime().
-     *
-     * @see AnimChannel#setTime(float)
-     */
-    public float getTime() {
-        return time;
-    }
-
-    /**
-     * @param time Set the time of the currently playing animation, the time
-     * is clamped from 0 to {@link #getAnimMaxTime()}. 
-     */
-    public void setTime(float time) {
-        this.time = FastMath.clamp(time, 0, getAnimMaxTime());
-    }
-
-    /**
-     * @return The length of the currently playing animation, or zero
-     * if no animation is playing.
-     *
-     * @see AnimChannel#getTime()
-     */
-    public float getAnimMaxTime(){
-        return animation != null ? animation.getLength() : 0f;
-    }
-
-    /**
-     * Set the current animation that is played by this AnimChannel.
-     * <p>
-     * This resets the time to zero, and optionally blends the animation
-     * over <code>blendTime</code> seconds with the currently playing animation.
-     * Notice that this method will reset the control's speed to 1.0.
-     *
-     * @param name The name of the animation to play
-     * @param blendTime The blend time over which to blend the new animation
-     * with the old one. If zero, then no blending will occur and the new
-     * animation will be applied instantly.
-     */
-    public void setAnim(String name, float blendTime){
-        if (name == null)
-            throw new IllegalArgumentException("name cannot be null");
-
-        if (blendTime < 0f)
-            throw new IllegalArgumentException("blendTime cannot be less than zero");
-
-        Animation anim = control.animationMap.get(name);
-        if (anim == null)
-            throw new IllegalArgumentException("Cannot find animation named: '"+name+"'");
-
-        control.notifyAnimChange(this, name);
-
-        if (animation != null && blendTime > 0f){
-            this.blendTime = blendTime;
-            // activate blending
-            blendTime = Math.min(blendTime, anim.getLength() / speed);            
-            blendFrom = animation;
-            timeBlendFrom = time;
-            speedBlendFrom = speed;
-            loopModeBlendFrom = loopMode;
-            blendAmount = 0f;
-            blendRate   = 1f / blendTime;
-        }else{
-            blendFrom = null;
-        }
-
-        animation = anim;
-        time = 0;
-        speed = 1f;
-        loopMode = LoopMode.Loop;
-        notified = false;
-    }
-
-    /**
-     * Set the current animation that is played by this AnimChannel.
-     * <p>
-     * See {@link #setAnim(java.lang.String, float)}.
-     * The blendTime argument by default is 150 milliseconds.
-     * 
-     * @param name The name of the animation to play
-     */
-    public void setAnim(String name){
-        setAnim(name, DEFAULT_BLEND_TIME);
-    }
-
-    /**
-     * Add all the bones of the model's skeleton to be
-     * influenced by this animation channel.
-     */
-    public void addAllBones() {
-        affectedBones = null;
-    }
-
-    /**
-     * Add a single bone to be influenced by this animation channel.
-     */
-    public void addBone(String name) {
-        addBone(control.getSkeleton().getBone(name));
-    }
-
-    /**
-     * Add a single bone to be influenced by this animation channel.
-     */
-    public void addBone(Bone bone) {
-        int boneIndex = control.getSkeleton().getBoneIndex(bone);
-        if(affectedBones == null) {
-            affectedBones = new BitSet(control.getSkeleton().getBoneCount());
-        }
-        affectedBones.set(boneIndex);
-    }
-
-    /**
-     * Add bones to be influenced by this animation channel starting from the
-     * given bone name and going toward the root bone.
-     */
-    public void addToRootBone(String name) {
-        addToRootBone(control.getSkeleton().getBone(name));
-    }
-
-    /**
-     * Add bones to be influenced by this animation channel starting from the
-     * given bone and going toward the root bone.
-     */
-    public void addToRootBone(Bone bone) {
-        addBone(bone);
-        while (bone.getParent() != null) {
-            bone = bone.getParent();
-            addBone(bone);
-        }
-    }
-
-    /**
-     * Add bones to be influenced by this animation channel, starting
-     * from the given named bone and going toward its children.
-     */
-    public void addFromRootBone(String name) {
-        addFromRootBone(control.getSkeleton().getBone(name));
-    }
-
-    /**
-     * Add bones to be influenced by this animation channel, starting
-     * from the given bone and going toward its children.
-     */
-    public void addFromRootBone(Bone bone) {
-        addBone(bone);
-        if (bone.getChildren() == null)
-            return;
-        for (Bone childBone : bone.getChildren()) {
-            addBone(childBone);
-            addFromRootBone(childBone);
-        }
-    }
-
-    BitSet getAffectedBones(){
-        return affectedBones;
-    }
-    
-    public void reset(boolean rewind){
-        if(rewind){
-            setTime(0);        
-            if(control.getSkeleton()!=null){
-                control.getSkeleton().resetAndUpdate();
-            }else{
-                TempVars vars = TempVars.get();
-                update(0, vars);
-                vars.release();    
-            }
-        }
-        animation = null;
-        notified = false;
-    }
-
-    void update(float tpf, TempVars vars) {
-        if (animation == null)
-            return;
-
-        if (blendFrom != null && blendAmount != 1.0f){
-            // The blendFrom anim is set, the actual animation
-            // playing will be set 
-//            blendFrom.setTime(timeBlendFrom, 1f, control, this, vars);
-            blendFrom.setTime(timeBlendFrom, 1f - blendAmount, control, this, vars);
-            
-            timeBlendFrom += tpf * speedBlendFrom;
-            timeBlendFrom = AnimationUtils.clampWrapTime(timeBlendFrom,
-                                          blendFrom.getLength(),
-                                          loopModeBlendFrom);
-            if (timeBlendFrom < 0){
-                timeBlendFrom = -timeBlendFrom;
-                speedBlendFrom = -speedBlendFrom;
-            }
-
-            blendAmount += tpf * blendRate;
-            if (blendAmount > 1f){
-                blendAmount = 1f;
-                blendFrom = null;
-            }
-        }
-        
-        animation.setTime(time, blendAmount, control, this, vars);
-        time += tpf * speed;      
-        if (animation.getLength() > 0){
-            if (!notified && (time >= animation.getLength() || time < 0)) {
-                if (loopMode == LoopMode.DontLoop) {
-                    // Note that this flag has to be set before calling the notify
-                    // since the notify may start a new animation and then unset
-                    // the flag.
-                    notified = true;
-                }
-                control.notifyAnimCycleDone(this, animation.getName());
-            } 
-        }
-        time = AnimationUtils.clampWrapTime(time, animation.getLength(), loopMode);
-        if (time < 0){
-            // Negative time indicates that speed should be inverted
-            // (for cycle loop mode only)
-            time = -time;
-            speed = -speed;
-        }
-    }
-}
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.animation;
+
+import com.jme3.math.FastMath;
+import com.jme3.util.TempVars;
+import java.util.BitSet;
+
+/**
+ * <code>AnimChannel</code> provides controls, such as play, pause,
+ * fast forward, etc, for an animation. The animation
+ * channel may influence the entire model or specific bones of the model's
+ * skeleton. A single model may have multiple animation channels influencing
+ * various parts of its body. For example, a character model may have an
+ * animation channel for its feet, and another for its torso, and
+ * the animations for each channel are controlled independently.
+ * 
+ * @author Kirill Vainer
+ */
+public final class AnimChannel {
+
+    private static final float DEFAULT_BLEND_TIME = 0.15f;
+    
+    private AnimControl control;
+
+    private BitSet affectedBones;
+
+    private Animation animation;
+    private Animation blendFrom;
+    private float time;
+    private float speed;
+    private float timeBlendFrom;
+    private float blendTime;
+    private float speedBlendFrom;
+    private boolean notified=false;
+
+    private LoopMode loopMode, loopModeBlendFrom;
+    
+    private float blendAmount = 1f;
+    private float blendRate   = 0;
+    
+    public AnimChannel(){
+        
+    }
+    
+    public AnimChannel(AnimControl control){
+        this.control = control;
+    }
+
+    /**
+     * Returns the parent control of this AnimChannel.
+     * 
+     * @return the parent control of this AnimChannel.
+     * @see AnimControl
+     */
+    public AnimControl getControl() {
+        return control;
+    }
+    
+    /**
+     * @return The name of the currently playing animation, or null if
+     * none is assigned.
+     *
+     * @see AnimChannel#setAnim(java.lang.String) 
+     */
+    public String getAnimationName() {
+        return animation != null ? animation.getName() : null;
+    }
+
+    /**
+     * @return The loop mode currently set for the animation. The loop mode
+     * determines what will happen to the animation once it finishes
+     * playing.
+     * 
+     * For more information, see the LoopMode enum class.
+     * @see LoopMode
+     * @see AnimChannel#setLoopMode(com.jme3.animation.LoopMode)
+     */
+    public LoopMode getLoopMode() {
+        return loopMode;
+    }
+
+    /**
+     * @param loopMode Set the loop mode for the channel. The loop mode
+     * determines what will happen to the animation once it finishes
+     * playing.
+     *
+     * For more information, see the LoopMode enum class.
+     * @see LoopMode
+     */
+    public void setLoopMode(LoopMode loopMode) {
+        this.loopMode = loopMode;
+    }
+
+    /**
+     * @return The speed that is assigned to the animation channel. The speed
+     * is a scale value starting from 0.0, at 1.0 the animation will play
+     * at its default speed.
+     *
+     * @see AnimChannel#setSpeed(float)
+     */
+    public float getSpeed() {
+        return speed;
+    }
+
+    /**
+     * @param speed Set the speed of the animation channel. The speed
+     * is a scale value starting from 0.0, at 1.0 the animation will play
+     * at its default speed.
+     */
+    public void setSpeed(float speed) {
+        this.speed = speed;
+        if(blendTime>0){
+            this.speedBlendFrom = speed;
+            blendTime = Math.min(blendTime, animation.getLength() / speed);  
+            blendRate = 1/ blendTime;
+        }
+    }
+
+    /**
+     * @return The time of the currently playing animation. The time
+     * starts at 0 and continues on until getAnimMaxTime().
+     *
+     * @see AnimChannel#setTime(float)
+     */
+    public float getTime() {
+        return time;
+    }
+
+    /**
+     * @param time Set the time of the currently playing animation, the time
+     * is clamped from 0 to {@link #getAnimMaxTime()}. 
+     */
+    public void setTime(float time) {
+        this.time = FastMath.clamp(time, 0, getAnimMaxTime());
+    }
+
+    /**
+     * @return The length of the currently playing animation, or zero
+     * if no animation is playing.
+     *
+     * @see AnimChannel#getTime()
+     */
+    public float getAnimMaxTime(){
+        return animation != null ? animation.getLength() : 0f;
+    }
+
+    /**
+     * Set the current animation that is played by this AnimChannel.
+     * <p>
+     * This resets the time to zero, and optionally blends the animation
+     * over <code>blendTime</code> seconds with the currently playing animation.
+     * Notice that this method will reset the control's speed to 1.0.
+     *
+     * @param name The name of the animation to play
+     * @param blendTime The blend time over which to blend the new animation
+     * with the old one. If zero, then no blending will occur and the new
+     * animation will be applied instantly.
+     */
+    public void setAnim(String name, float blendTime){
+        if (name == null)
+            throw new IllegalArgumentException("name cannot be null");
+
+        if (blendTime < 0f)
+            throw new IllegalArgumentException("blendTime cannot be less than zero");
+
+        Animation anim = control.animationMap.get(name);
+        if (anim == null)
+            throw new IllegalArgumentException("Cannot find animation named: '"+name+"'");
+
+        control.notifyAnimChange(this, name);
+
+        if (animation != null && blendTime > 0f){
+            this.blendTime = blendTime;
+            // activate blending
+            blendTime = Math.min(blendTime, anim.getLength() / speed);            
+            blendFrom = animation;
+            timeBlendFrom = time;
+            speedBlendFrom = speed;
+            loopModeBlendFrom = loopMode;
+            blendAmount = 0f;
+            blendRate   = 1f / blendTime;
+        }else{
+            blendFrom = null;
+        }
+
+        animation = anim;
+        time = 0;
+        speed = 1f;
+        loopMode = LoopMode.Loop;
+        notified = false;
+    }
+
+    /**
+     * Set the current animation that is played by this AnimChannel.
+     * <p>
+     * See {@link #setAnim(java.lang.String, float)}.
+     * The blendTime argument by default is 150 milliseconds.
+     * 
+     * @param name The name of the animation to play
+     */
+    public void setAnim(String name){
+        setAnim(name, DEFAULT_BLEND_TIME);
+    }
+
+    /**
+     * Add all the bones of the model's skeleton to be
+     * influenced by this animation channel.
+     */
+    public void addAllBones() {
+        affectedBones = null;
+    }
+
+    /**
+     * Add a single bone to be influenced by this animation channel.
+     */
+    public void addBone(String name) {
+        addBone(control.getSkeleton().getBone(name));
+    }
+
+    /**
+     * Add a single bone to be influenced by this animation channel.
+     */
+    public void addBone(Bone bone) {
+        int boneIndex = control.getSkeleton().getBoneIndex(bone);
+        if(affectedBones == null) {
+            affectedBones = new BitSet(control.getSkeleton().getBoneCount());
+        }
+        affectedBones.set(boneIndex);
+    }
+
+    /**
+     * Add bones to be influenced by this animation channel starting from the
+     * given bone name and going toward the root bone.
+     */
+    public void addToRootBone(String name) {
+        addToRootBone(control.getSkeleton().getBone(name));
+    }
+
+    /**
+     * Add bones to be influenced by this animation channel starting from the
+     * given bone and going toward the root bone.
+     */
+    public void addToRootBone(Bone bone) {
+        addBone(bone);
+        while (bone.getParent() != null) {
+            bone = bone.getParent();
+            addBone(bone);
+        }
+    }
+
+    /**
+     * Add bones to be influenced by this animation channel, starting
+     * from the given named bone and going toward its children.
+     */
+    public void addFromRootBone(String name) {
+        addFromRootBone(control.getSkeleton().getBone(name));
+    }
+
+    /**
+     * Add bones to be influenced by this animation channel, starting
+     * from the given bone and going toward its children.
+     */
+    public void addFromRootBone(Bone bone) {
+        addBone(bone);
+        if (bone.getChildren() == null)
+            return;
+        for (Bone childBone : bone.getChildren()) {
+            addBone(childBone);
+            addFromRootBone(childBone);
+        }
+    }
+
+    BitSet getAffectedBones(){
+        return affectedBones;
+    }
+    
+    public void reset(boolean rewind){
+        if(rewind){
+            setTime(0);        
+            if(control.getSkeleton()!=null){
+                control.getSkeleton().resetAndUpdate();
+            }else{
+                TempVars vars = TempVars.get();
+                update(0, vars);
+                vars.release();    
+            }
+        }
+        animation = null;
+        notified = false;
+    }
+
+    void update(float tpf, TempVars vars) {
+        if (animation == null)
+            return;
+
+        if (blendFrom != null && blendAmount != 1.0f){
+            // The blendFrom anim is set, the actual animation
+            // playing will be set 
+//            blendFrom.setTime(timeBlendFrom, 1f, control, this, vars);
+            blendFrom.setTime(timeBlendFrom, 1f - blendAmount, control, this, vars);
+            
+            timeBlendFrom += tpf * speedBlendFrom;
+            timeBlendFrom = AnimationUtils.clampWrapTime(timeBlendFrom,
+                                          blendFrom.getLength(),
+                                          loopModeBlendFrom);
+            if (timeBlendFrom < 0){
+                timeBlendFrom = -timeBlendFrom;
+                speedBlendFrom = -speedBlendFrom;
+            }
+
+            blendAmount += tpf * blendRate;
+            if (blendAmount > 1f){
+                blendAmount = 1f;
+                blendFrom = null;
+            }
+        }
+        
+        animation.setTime(time, blendAmount, control, this, vars);
+        time += tpf * speed;      
+        if (animation.getLength() > 0){
+            if (!notified && (time >= animation.getLength() || time < 0)) {
+                if (loopMode == LoopMode.DontLoop) {
+                    // Note that this flag has to be set before calling the notify
+                    // since the notify may start a new animation and then unset
+                    // the flag.
+                    notified = true;
+                }
+                control.notifyAnimCycleDone(this, animation.getName());
+            } 
+        }
+        time = AnimationUtils.clampWrapTime(time, animation.getLength(), loopMode);
+        if (time < 0){
+            // Negative time indicates that speed should be inverted
+            // (for cycle loop mode only)
+            time = -time;
+            speed = -speed;
+        }
+    }
+}

+ 3 - 1
jme3-core/src/main/java/com/jme3/animation/AnimationUtils.java

@@ -41,7 +41,9 @@ import static com.jme3.animation.LoopMode.Loop;
  */
 public class AnimationUtils {
     
-    
+    public AnimationUtils(){
+        
+    }
     /**
      * Clamps the time according to duration and loopMode
      * @param time

+ 706 - 720
jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java

@@ -1,720 +1,706 @@
-/*
- * Copyright (c) 2009-2012 jMonkeyEngine
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * * Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- *
- * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
- *   may be used to endorse or promote products derived from this software
- *   without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.jme3.cinematic;
-
-import com.jme3.animation.LoopMode;
-import com.jme3.app.Application;
-import com.jme3.app.state.AppState;
-import com.jme3.app.state.AppStateManager;
-import com.jme3.cinematic.events.AbstractCinematicEvent;
-import com.jme3.cinematic.events.CinematicEvent;
-import com.jme3.export.*;
-import com.jme3.renderer.Camera;
-import com.jme3.renderer.RenderManager;
-import com.jme3.scene.CameraNode;
-import com.jme3.scene.Node;
-import com.jme3.scene.control.CameraControl;
-import com.jme3.scene.control.CameraControl.ControlDirection;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-/**
- * An appstate for composing and playing cut scenes in a game. The cinematic
- * schedules CinematicEvents over a timeline. Once the Cinematic created it has
- * to be attached to the stateManager.
- *
- * You can add various CinematicEvents to a Cinematic, see package
- * com.jme3.cinematic.events
- *
- * Two main methods can be used to add an event :
- *
- * @see Cinematic#addCinematicEvent(float,
- * com.jme3.cinematic.events.CinematicEvent) , that adds an event at the given
- * time form the cinematic start.
- *
- * @see
- * Cinematic#enqueueCinematicEvent(com.jme3.cinematic.events.CinematicEvent)
- * that enqueue events one after the other according to their initialDuration
- *
- * a cinematic has convenient methods to handle the playback :
- * @see Cinematic#play()
- * @see Cinematic#pause()
- * @see Cinematic#stop()
- *
- * A cinematic is itself a CinematicEvent, meaning you can embed several
- * Cinematics Embed cinematics must not be added to the stateManager though.
- *
- * Cinematic has a way to handle several point of view by creating CameraNode
- * over a cam and activating them on schedule.
- * @see Cinematic#bindCamera(java.lang.String, com.jme3.renderer.Camera)
- * @see Cinematic#activateCamera(float, java.lang.String)
- * @see Cinematic#setActiveCamera(java.lang.String)
- *
- * @author Nehon
- */
-public class Cinematic extends AbstractCinematicEvent implements AppState {
-
-    private static final Logger logger = Logger.getLogger(Application.class.getName());
-    private Node scene;
-    protected TimeLine timeLine = new TimeLine();
-    private int lastFetchedKeyFrame = -1;
-    private List<CinematicEvent> cinematicEvents = new ArrayList<CinematicEvent>();
-    private Map<String, CameraNode> cameras = new HashMap<String, CameraNode>();
-    private CameraNode currentCam;
-    private boolean initialized = false;
-    private Map<String, Map<Object, Object>> eventsData;
-    private float nextEnqueue = 0;
-
-    /**
-     * Used for serialization creates a cinematic, don't use this constructor
-     * directly
-     */
-    public Cinematic() {
-    }
-
-    /**
-     * creates a cinematic
-     *
-     * @param scene the scene in which the cinematic should take place
-     */
-    public Cinematic(Node scene) {
-        this.scene = scene;
-    }
-
-    /**
-     * creates a cinematic
-     *
-     * @param scene the scene in which the cinematic should take place
-     * @param initialDuration the duration of the cinematic (without considering
-     * the speed)
-     */
-    public Cinematic(Node scene, float initialDuration) {
-        super(initialDuration);
-        this.scene = scene;
-    }
-
-    /**
-     * creates a cinematic
-     *
-     * @param scene the scene in which the cinematic should take place
-     * @param loopMode tells if this cinematic should be looped or not
-     */
-    public Cinematic(Node scene, LoopMode loopMode) {
-        super(loopMode);
-        this.scene = scene;
-    }
-
-    /**
-     * creates a cinematic
-     *
-     * @param scene the scene in which the cinematic should take place
-     * @param initialDuration the duration of the cinematic (without considering
-     * the speed)
-     * @param loopMode tells if this cinematic should be looped or not
-     */
-    public Cinematic(Node scene, float initialDuration, LoopMode loopMode) {
-        super(initialDuration, loopMode);
-        this.scene = scene;
-    }
-
-    /**
-     * called internally
-     */
-    @Override
-    public void onPlay() {
-        if (isInitialized()) {
-            if (playState == PlayState.Paused) {
-                for (int i = 0; i < cinematicEvents.size(); i++) {
-                    CinematicEvent ce = cinematicEvents.get(i);
-                    if (ce.getPlayState() == PlayState.Paused) {
-                        ce.play();
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * called internally
-     */
-    @Override
-    public void onStop() {
-        time = 0;
-        lastFetchedKeyFrame = -1;
-        for (int i = 0; i < cinematicEvents.size(); i++) {
-            CinematicEvent ce = cinematicEvents.get(i);
-            ce.setTime(0);
-            ce.forceStop();
-        }
-        setEnableCurrentCam(false);
-    }
-
-    /**
-     * called internally
-     */
-    @Override
-    public void onPause() {
-        for (int i = 0; i < cinematicEvents.size(); i++) {
-            CinematicEvent ce = cinematicEvents.get(i);
-            if (ce.getPlayState() == PlayState.Playing) {
-                ce.pause();
-            }
-        }
-    }
-
-    /**
-     * used internally for serialization
-     *
-     * @param ex
-     * @throws IOException
-     */
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-
-        oc.writeSavableArrayList((ArrayList) cinematicEvents, "cinematicEvents", null);
-        oc.writeStringSavableMap(cameras, "cameras", null);
-        oc.write(timeLine, "timeLine", null);
-
-    }
-
-    /**
-     * used internally for serialization
-     *
-     * @param im
-     * @throws IOException
-     */
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        InputCapsule ic = im.getCapsule(this);
-
-        cinematicEvents = ic.readSavableArrayList("cinematicEvents", null);
-        cameras = (Map<String, CameraNode>) ic.readStringSavableMap("cameras", null);
-        timeLine = (TimeLine) ic.readSavable("timeLine", null);
-    }
-
-    /**
-     * sets the speed of the cinematic. Note that it will set the speed of all
-     * events in the cinematic. 1 is normal speed. use 0.5f to make the
-     * cinematic twice slower, use 2 to make it twice faster
-     *
-     * @param speed the speed
-     */
-    @Override
-    public void setSpeed(float speed) {
-        super.setSpeed(speed);
-        for (int i = 0; i < cinematicEvents.size(); i++) {
-            CinematicEvent ce = cinematicEvents.get(i);
-            ce.setSpeed(speed);
-        }
-
-
-    }
-
-    /**
-     * used internally
-     *
-     * @param stateManager the state manager
-     * @param app the application
-     */
-    public void initialize(AppStateManager stateManager, Application app) {
-        initEvent(app, this);
-        for (CinematicEvent cinematicEvent : cinematicEvents) {
-            cinematicEvent.initEvent(app, this);
-        }
-
-        initialized = true;
-    }
-
-    /**
-     * used internally
-     *
-     * @return
-     */
-    public boolean isInitialized() {
-        return initialized;
-    }
-
-    /**
-     * passing true has the same effect as play() you should use play(),
-     * pause(), stop() to handle the cinematic playing state.
-     *
-     * @param enabled true or false
-     */
-    public void setEnabled(boolean enabled) {
-        if (enabled) {
-            play();
-        }
-    }
-
-    /**
-     * return true if the cinematic appstate is enabled (the cinematic is
-     * playing)
-     *
-     * @return true if enabled
-     */
-    public boolean isEnabled() {
-        return playState == PlayState.Playing;
-    }
-
-    /**
-     * called internally
-     *
-     * @param stateManager the state manager
-     */
-    public void stateAttached(AppStateManager stateManager) {
-    }
-
-    /**
-     * called internally
-     *
-     * @param stateManager the state manager
-     */
-    public void stateDetached(AppStateManager stateManager) {
-        stop();
-    }
-
-    /**
-     * called internally don't call it directly.
-     *
-     * @param tpf
-     */
-    public void update(float tpf) {
-        if (isInitialized()) {
-            internalUpdate(tpf);
-        }
-    }
-
-    /**
-     * used internally, don't call this directly.
-     *
-     * @param tpf
-     */
-    @Override
-    public void onUpdate(float tpf) {
-        int keyFrameIndex = timeLine.getKeyFrameIndexFromTime(time);
-
-        //iterate to make sure every key frame is triggered
-        for (int i = lastFetchedKeyFrame + 1; i <= keyFrameIndex; i++) {
-            KeyFrame keyFrame = timeLine.get(i);
-            if (keyFrame != null) {
-                keyFrame.trigger();
-            }
-        }
-
-        
-        for (int i = 0; i < cinematicEvents.size(); i++) {
-            CinematicEvent ce = cinematicEvents.get(i);
-            ce.internalUpdate(tpf);
-        }
-
-        
-        lastFetchedKeyFrame = keyFrameIndex;
-    }
-
-    /**
-     * This is used internally but can be called to shuffle through the
-     * cinematic.
-     *
-     * @param time the time to shuffle to.
-     */
-    @Override
-    public void setTime(float time) {
-
-        //stopping all events
-        onStop();
-        super.setTime(time);
-
-        int keyFrameIndex = timeLine.getKeyFrameIndexFromTime(time);
-        //triggering all the event from start to "time" 
-        //then computing timeOffset for each event
-        for (int i = 0; i <= keyFrameIndex; i++) {
-            KeyFrame keyFrame = timeLine.get(i);
-            if (keyFrame != null) {
-                for (CinematicEvent ce : keyFrame.getCinematicEvents()) {
-                    float t = this.time - timeLine.getKeyFrameTime(keyFrame);
-                    if (t >= 0 && (t <= ce.getInitialDuration() || ce.getLoopMode() != LoopMode.DontLoop)) {
-                        ce.play();
-                    }
-                    ce.setTime(t);
-                }
-            }
-        }
-        lastFetchedKeyFrame = keyFrameIndex;
-        if (playState != PlayState.Playing) {
-            pause();
-        }
-    }
-
-    /**
-     * Adds a cinematic event to this cinematic at the given timestamp. This
-     * operation returns a keyFrame
-     *
-     * @param timeStamp the time when the event will start after the beginning of
-     * the cinematic
-     * @param cinematicEvent the cinematic event
-     * @return the keyFrame for that event.
-     */
-    public KeyFrame addCinematicEvent(float timeStamp, CinematicEvent cinematicEvent) {
-        KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp);
-        if (keyFrame == null) {
-            keyFrame = new KeyFrame();
-            timeLine.addKeyFrameAtTime(timeStamp, keyFrame);
-        }
-        keyFrame.cinematicEvents.add(cinematicEvent);
-        cinematicEvents.add(cinematicEvent);
-        if (isInitialized()) {
-            cinematicEvent.initEvent(null, this);
-        }
-        return keyFrame;
-    }
-
-    /**
-     * enqueue a cinematic event to a cinematic. This is a handy method when you
-     * want to chain event of a given duration without knowing their initial
-     * duration
-     *
-     * @param cinematicEvent the cinematic event to enqueue
-     * @return the timestamp the event was scheduled.
-     */
-    public float enqueueCinematicEvent(CinematicEvent cinematicEvent) {
-        float scheduleTime = nextEnqueue;
-        addCinematicEvent(scheduleTime, cinematicEvent);
-        nextEnqueue += cinematicEvent.getInitialDuration();
-        return scheduleTime;
-    }
-
-    /**
-     * removes the first occurrence found of the given cinematicEvent.
-     *
-     * @param cinematicEvent the cinematicEvent to remove
-     * @return true if the element has been removed
-     */
-    public boolean removeCinematicEvent(CinematicEvent cinematicEvent) {
-        cinematicEvent.dispose();
-        cinematicEvents.remove(cinematicEvent);
-        for (KeyFrame keyFrame : timeLine.values()) {
-            if (keyFrame.cinematicEvents.remove(cinematicEvent)) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * removes the first occurrence found of the given cinematicEvent for the
-     * given time stamp.
-     *
-     * @param timeStamp the timestamp when the cinematicEvent has been added
-     * @param cinematicEvent the cinematicEvent to remove
-     * @return true if the element has been removed
-     */
-    public boolean removeCinematicEvent(float timeStamp, CinematicEvent cinematicEvent) {
-        cinematicEvent.dispose();
-        KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp);
-        return removeCinematicEvent(keyFrame, cinematicEvent);
-    }
-
-    /**
-     * removes the first occurrence found of the given cinematicEvent for the
-     * given keyFrame
-     *
-     * @param keyFrame the keyFrame returned by the addCinematicEvent method.
-     * @param cinematicEvent the cinematicEvent to remove
-     * @return true if the element has been removed
-     */
-    public boolean removeCinematicEvent(KeyFrame keyFrame, CinematicEvent cinematicEvent) {
-        cinematicEvent.dispose();
-        boolean ret = keyFrame.cinematicEvents.remove(cinematicEvent);
-        cinematicEvents.remove(cinematicEvent);
-        if (keyFrame.isEmpty()) {
-            timeLine.removeKeyFrame(keyFrame.getIndex());
-        }
-        return ret;
-    }
-
-    /**
-     * called internally
-     *
-     * @see AppState#render(com.jme3.renderer.RenderManager) 
-     */
-    public void render(RenderManager rm) {
-    }
-
-    /**
-     * called internally
-     *
-     * @see AppState#postRender()
-     */
-    public void postRender() {
-    }
-
-    /**
-     * called internally
-     *
-     * @see AppState#cleanup()
-     */
-    public void cleanup() {
-    }
-
-    /**
-     * fits the duration of the cinematic to the duration of all its child
-     * cinematic events
-     */
-    public void fitDuration() {
-        KeyFrame kf = timeLine.getKeyFrameAtIndex(timeLine.getLastKeyFrameIndex());
-        float d = 0;
-        for (int i = 0; i < kf.getCinematicEvents().size(); i++) {
-            CinematicEvent ce = kf.getCinematicEvents().get(i);
-            float dur = timeLine.getKeyFrameTime(kf) + ce.getDuration() * ce.getSpeed();
-            if (d < dur) {
-                d = dur;
-            }
-        }
-
-        initialDuration = d;
-    }
-
-    /**
-     * Binds a camera to this cinematic, tagged by a unique name. This methods
-     * creates and returns a CameraNode for the cam and attach it to the scene.
-     * The control direction is set to SpatialToCamera. This camera Node can
-     * then be used in other events to handle the camera movements during the
-     * playback
-     *
-     * @param cameraName the unique tag the camera should have
-     * @param cam the scene camera.
-     * @return the created CameraNode.
-     */
-    public CameraNode bindCamera(String cameraName, Camera cam) {
-        if (cameras.containsKey(cameraName)) {
-            throw new IllegalArgumentException("Camera " + cameraName + " is already binded to this cinematic");
-        }
-        CameraNode node = new CameraNode(cameraName, cam);
-        node.setControlDir(ControlDirection.SpatialToCamera);
-        node.getControl(CameraControl.class).setEnabled(false);
-        cameras.put(cameraName, node);
-        scene.attachChild(node);
-        return node;
-    }
-
-    /**
-     * returns a cameraNode given its name
-     *
-     * @param cameraName the camera name (as registered in
-     * Cinematic#bindCamera())
-     * @return the cameraNode for this name
-     */
-    public CameraNode getCamera(String cameraName) {
-        return cameras.get(cameraName);
-    }
-
-    /**
-     * enable/disable the camera control of the cameraNode of the current cam
-     *
-     * @param enabled
-     */
-    private void setEnableCurrentCam(boolean enabled) {
-        if (currentCam != null) {
-            currentCam.getControl(CameraControl.class).setEnabled(enabled);
-        }
-    }
-
-    /**
-     * Sets the active camera instantly (use activateCamera if you want to
-     * schedule that event)
-     *
-     * @param cameraName the camera name (as registered in
-     * Cinematic#bindCamera())
-     */
-    public void setActiveCamera(String cameraName) {
-        setEnableCurrentCam(false);
-        currentCam = cameras.get(cameraName);
-        if (currentCam == null) {
-            logger.log(Level.WARNING, "{0} is not a camera bond to the cinematic, cannot activate", cameraName);
-        }
-        setEnableCurrentCam(true);
-    }
-
-    /**
-     * schedule an event that will activate the camera at the given time
-     *
-     * @param timeStamp the time to activate the cam
-     * @param cameraName the camera name (as registered in
-     * Cinematic#bindCamera())
-     */
-    public void activateCamera(final float timeStamp, final String cameraName) {
-        addCinematicEvent(timeStamp, new AbstractCinematicEvent() {
-            @Override
-            public void play() {
-                super.play();
-                stop();
-            }
-
-            @Override
-            public void onPlay() {
-                setActiveCamera(cameraName);
-            }
-
-            @Override
-            public void onUpdate(float tpf) {
-            }
-
-            @Override
-            public void onStop() {
-            }
-
-            @Override
-            public void onPause() {
-            }
-
-            @Override
-            public void forceStop() {
-            }
-
-            @Override
-            public void setTime(float time) {
-                play();
-            }
-        });
-    }
-
-    /**
-     * returns the complete eventdata map
-     *
-     * @return the eventdata map
-     */
-    private Map<String, Map<Object, Object>> getEventsData() {
-        if (eventsData == null) {
-            eventsData = new HashMap<String, Map<Object, Object>>();
-        }
-        return eventsData;
-    }
-
-    /**
-     * used internally put an eventdata in the cinematic
-     *
-     * @param type the type of data
-     * @param key the key
-     * @param object the data
-     */
-    public void putEventData(String type, Object key, Object object) {
-        Map<String, Map<Object, Object>> data = getEventsData();
-        Map<Object, Object> row = data.get(type);
-        if (row == null) {
-            row = new HashMap<Object, Object>();
-        }
-        row.put(key, object);
-        data.put(type, row);
-    }
-
-    /**
-     * used internally return and event data
-     *
-     * @param type the type of data
-     * @param key the key
-     * @return
-     */
-    public Object getEventData(String type, Object key) {
-        if (eventsData != null) {
-            Map<Object, Object> row = eventsData.get(type);
-            if (row != null) {
-                return row.get(key);
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Used internally remove an eventData
-     *
-     * @param type the type of data
-     * @param key the key of the data
-     */
-    public void removeEventData(String type, Object key) {
-        if (eventsData != null) {
-            Map<Object, Object> row = eventsData.get(type);
-            if (row != null) {
-                row.remove(key);
-            }
-        }
-    }
-
-    /**
-     * sets the scene to use for this cinematic it is expected that the scene is
-     * added before adding events to the cinematic
-     *
-     * @param scene the scene where the cinematic should take place.
-     */
-    public void setScene(Node scene) {
-        this.scene = scene;
-    }
-
-    /**
-     * return the scene where the cinematic occur
-     *
-     * @return the scene
-     */
-    public Node getScene() {
-        return scene;
-    }
-
-    /**
-     * clear the cinematic of its events.
-     */
-    public void clear() {
-        dispose();
-        cinematicEvents.clear();
-        timeLine.clear();
-        if (eventsData != null) {
-            eventsData.clear();
-        }
-    }
-
-    /**
-     * used internally to cleanup the cinematic. Called when the clear() method
-     * is called
-     */
-    @Override
-    public void dispose() {
-        for (CinematicEvent event : cinematicEvents) {
-            event.dispose();
-        }
-    }
-}
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.cinematic;
+
+import com.jme3.animation.LoopMode;
+import com.jme3.app.Application;
+import com.jme3.app.state.AppState;
+import com.jme3.app.state.AppStateManager;
+import com.jme3.cinematic.events.AbstractCinematicEvent;
+import com.jme3.cinematic.events.CameraEvent;
+import com.jme3.cinematic.events.CinematicEvent;
+import com.jme3.export.*;
+import com.jme3.renderer.Camera;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.CameraNode;
+import com.jme3.scene.Node;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.CameraControl;
+import com.jme3.scene.control.CameraControl.ControlDirection;
+import com.jme3.scene.control.Control;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * An appstate for composing and playing cut scenes in a game. The cinematic
+ * schedules CinematicEvents over a timeline. Once the Cinematic created it has
+ * to be attached to the stateManager.
+ *
+ * You can add various CinematicEvents to a Cinematic, see package
+ * com.jme3.cinematic.events
+ *
+ * Two main methods can be used to add an event :
+ *
+ * @see Cinematic#addCinematicEvent(float,
+ * com.jme3.cinematic.events.CinematicEvent) , that adds an event at the given
+ * time form the cinematic start.
+ *
+ * @see
+ * Cinematic#enqueueCinematicEvent(com.jme3.cinematic.events.CinematicEvent)
+ * that enqueue events one after the other according to their initialDuration
+ *
+ * a cinematic has convenient methods to handle the playback :
+ * @see Cinematic#play()
+ * @see Cinematic#pause()
+ * @see Cinematic#stop()
+ *
+ * A cinematic is itself a CinematicEvent, meaning you can embed several
+ * Cinematics Embed cinematics must not be added to the stateManager though.
+ *
+ * Cinematic has a way to handle several point of view by creating CameraNode
+ * over a cam and activating them on schedule.
+ * @see Cinematic#bindCamera(java.lang.String, com.jme3.renderer.Camera)
+ * @see Cinematic#activateCamera(float, java.lang.String)
+ * @see Cinematic#setActiveCamera(java.lang.String)
+ *
+ * @author Nehon
+ */
+public class Cinematic extends AbstractCinematicEvent implements AppState {
+
+    private static final Logger logger = Logger.getLogger(Cinematic.class.getName());
+    private Node scene;
+    protected TimeLine timeLine = new TimeLine();
+    private int lastFetchedKeyFrame = -1;
+    private List<CinematicEvent> cinematicEvents = new ArrayList<>();
+    private Map<String, CameraNode> cameras = new HashMap<>();
+    private CameraNode currentCam;
+    private boolean initialized = false;
+    private Map<String, Map<Object, Object>> eventsData;
+    private float nextEnqueue = 0;
+
+    /**
+     * Used for serialization creates a cinematic, don't use this constructor
+     * directly
+     */
+    public Cinematic() {
+        super();
+    }
+
+    public Cinematic(float initialDuration) {
+        super(initialDuration);
+    }
+
+    public Cinematic(LoopMode loopMode) {
+        super(loopMode);
+    }
+
+    public Cinematic(float initialDuration, LoopMode loopMode) {
+        super(initialDuration, loopMode);
+    }
+
+    /**
+     * creates a cinematic
+     *
+     * @param scene the scene in which the cinematic should take place
+     */
+    public Cinematic(Node scene) {
+        this.scene = scene;
+    }
+
+    /**
+     * creates a cinematic
+     *
+     * @param scene the scene in which the cinematic should take place
+     * @param initialDuration the duration of the cinematic (without considering
+     * the speed)
+     */
+    public Cinematic(Node scene, float initialDuration) {
+        super(initialDuration);
+        this.scene = scene;
+    }
+
+    /**
+     * creates a cinematic
+     *
+     * @param scene the scene in which the cinematic should take place
+     * @param loopMode tells if this cinematic should be looped or not
+     */
+    public Cinematic(Node scene, LoopMode loopMode) {
+        super(loopMode);
+        this.scene = scene;
+    }
+
+    /**
+     * creates a cinematic
+     *
+     * @param scene the scene in which the cinematic should take place
+     * @param initialDuration the duration of the cinematic (without considering
+     * the speed)
+     * @param loopMode tells if this cinematic should be looped or not
+     */
+    public Cinematic(Node scene, float initialDuration, LoopMode loopMode) {
+        super(initialDuration, loopMode);
+        this.scene = scene;
+    }
+
+    /**
+     * called internally
+     */
+    @Override
+    public void onPlay() {
+        if (isInitialized()) {
+            if (playState == PlayState.Paused) {
+                for (int i = 0; i < cinematicEvents.size(); i++) {
+                    CinematicEvent ce = cinematicEvents.get(i);
+                    if (ce.getPlayState() == PlayState.Paused) {
+                        ce.play();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * called internally
+     */
+    @Override
+    public void onStop() {
+        time = 0;
+        lastFetchedKeyFrame = -1;
+        for (int i = 0; i < cinematicEvents.size(); i++) {
+            CinematicEvent ce = cinematicEvents.get(i);
+            ce.setTime(0);
+            ce.forceStop();
+        }
+        setEnableCurrentCam(false);
+    }
+
+    /**
+     * called internally
+     */
+    @Override
+    public void onPause() {
+        for (int i = 0; i < cinematicEvents.size(); i++) {
+            CinematicEvent ce = cinematicEvents.get(i);
+            if (ce.getPlayState() == PlayState.Playing) {
+                ce.pause();
+            }
+        }
+    }
+
+    /**
+     * used internally for serialization
+     *
+     * @param ex
+     * @throws IOException
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(cinematicEvents.toArray(new CinematicEvent[cinematicEvents.size()]), "cinematicEvents", null);
+        oc.writeStringSavableMap(cameras, "cameras", null);
+        oc.write(timeLine, "timeLine", null);
+
+    }
+
+    /**
+     * used internally for serialization
+     *
+     * @param im
+     * @throws IOException
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+
+        Savable[] events = ic.readSavableArray("cinematicEvents", null);
+        for (Savable c : events) {
+//            addCinematicEvent(((CinematicEvent) c).getTime(), (CinematicEvent) c)
+            cinematicEvents.add((CinematicEvent) c);
+        }
+        cameras = (Map<String, CameraNode>) ic.readStringSavableMap("cameras", null);
+        timeLine = (TimeLine) ic.readSavable("timeLine", null);
+    }
+
+    /**
+     * sets the speed of the cinematic. Note that it will set the speed of all
+     * events in the cinematic. 1 is normal speed. use 0.5f to make the
+     * cinematic twice slower, use 2 to make it twice faster
+     *
+     * @param speed the speed
+     */
+    @Override
+    public void setSpeed(float speed) {
+        super.setSpeed(speed);
+        for (int i = 0; i < cinematicEvents.size(); i++) {
+            CinematicEvent ce = cinematicEvents.get(i);
+            ce.setSpeed(speed);
+        }
+
+    }
+
+    /**
+     * used internally
+     *
+     * @param stateManager the state manager
+     * @param app the application
+     */
+    public void initialize(AppStateManager stateManager, Application app) {
+        initEvent(app, this);
+        for (CinematicEvent cinematicEvent : cinematicEvents) {
+            cinematicEvent.initEvent(app, this);
+        }
+
+        initialized = true;
+    }
+
+    /**
+     * used internally
+     *
+     * @return
+     */
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+    /**
+     * passing true has the same effect as play() you should use play(),
+     * pause(), stop() to handle the cinematic playing state.
+     *
+     * @param enabled true or false
+     */
+    public void setEnabled(boolean enabled) {
+        if (enabled) {
+            play();
+        }
+    }
+
+    /**
+     * return true if the cinematic appstate is enabled (the cinematic is
+     * playing)
+     *
+     * @return true if enabled
+     */
+    public boolean isEnabled() {
+        return playState == PlayState.Playing;
+    }
+
+    /**
+     * called internally
+     *
+     * @param stateManager the state manager
+     */
+    public void stateAttached(AppStateManager stateManager) {
+    }
+
+    /**
+     * called internally
+     *
+     * @param stateManager the state manager
+     */
+    public void stateDetached(AppStateManager stateManager) {
+        stop();
+    }
+
+    /**
+     * called internally don't call it directly.
+     *
+     * @param tpf
+     */
+    @Override
+    public void update(float tpf) {
+        if (isInitialized()) {
+            internalUpdate(tpf);
+        }
+    }
+
+    /**
+     * used internally, don't call this directly.
+     *
+     * @param tpf
+     */
+    @Override
+    public void onUpdate(float tpf) {
+        int keyFrameIndex = timeLine.getKeyFrameIndexFromTime(time);
+
+        //iterate to make sure every key frame is triggered
+        for (int i = lastFetchedKeyFrame + 1; i <= keyFrameIndex; i++) {
+            KeyFrame keyFrame = timeLine.get(i);
+            if (keyFrame != null) {
+                keyFrame.trigger();
+            }
+        }
+
+        for (int i = 0; i < cinematicEvents.size(); i++) {
+            CinematicEvent ce = cinematicEvents.get(i);
+            ce.internalUpdate(tpf);
+        }
+
+        lastFetchedKeyFrame = keyFrameIndex;
+    }
+
+    /**
+     * This is used internally but can be called to shuffle through the
+     * cinematic.
+     *
+     * @param time the time to shuffle to.
+     */
+    @Override
+    public void setTime(float time) {
+
+        //stopping all events
+        onStop();
+        super.setTime(time);
+
+        int keyFrameIndex = timeLine.getKeyFrameIndexFromTime(time);
+        //triggering all the event from start to "time" 
+        //then computing timeOffset for each event
+        for (int i = 0; i <= keyFrameIndex; i++) {
+            KeyFrame keyFrame = timeLine.get(i);
+            if (keyFrame != null) {
+                for (CinematicEvent ce : keyFrame.getCinematicEvents()) {
+                    float t = this.time - timeLine.getKeyFrameTime(keyFrame);
+                    if (t >= 0 && (t <= ce.getInitialDuration() || ce.getLoopMode() != LoopMode.DontLoop)) {
+                        ce.play();
+                    }
+                    ce.setTime(t);
+                }
+            }
+        }
+        lastFetchedKeyFrame = keyFrameIndex;
+        if (playState != PlayState.Playing) {
+            pause();
+        }
+    }
+
+    /**
+     * Adds a cinematic event to this cinematic at the given timestamp. This
+     * operation returns a keyFrame
+     *
+     * @param timeStamp the time when the event will start after the beginning
+     * of the cinematic
+     * @param cinematicEvent the cinematic event
+     * @return the keyFrame for that event.
+     */
+    public KeyFrame addCinematicEvent(float timeStamp, CinematicEvent cinematicEvent) {
+        KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp);
+        if (keyFrame == null) {
+            keyFrame = new KeyFrame();
+            timeLine.addKeyFrameAtTime(timeStamp, keyFrame);
+        }
+        keyFrame.cinematicEvents.add(cinematicEvent);
+        cinematicEvents.add(cinematicEvent);
+        if (isInitialized()) {
+            cinematicEvent.initEvent(null, this);
+        }
+        return keyFrame;
+    }
+
+    /**
+     * enqueue a cinematic event to a cinematic. This is a handy method when you
+     * want to chain event of a given duration without knowing their initial
+     * duration
+     *
+     * @param cinematicEvent the cinematic event to enqueue
+     * @return the timestamp the event was scheduled.
+     */
+    public float enqueueCinematicEvent(CinematicEvent cinematicEvent) {
+        float scheduleTime = nextEnqueue;
+        addCinematicEvent(scheduleTime, cinematicEvent);
+        nextEnqueue += cinematicEvent.getInitialDuration();
+        return scheduleTime;
+    }
+
+    /**
+     * removes the first occurrence found of the given cinematicEvent.
+     *
+     * @param cinematicEvent the cinematicEvent to remove
+     * @return true if the element has been removed
+     */
+    public boolean removeCinematicEvent(CinematicEvent cinematicEvent) {
+        cinematicEvent.dispose();
+        cinematicEvents.remove(cinematicEvent);
+        for (KeyFrame keyFrame : timeLine.values()) {
+            if (keyFrame.cinematicEvents.remove(cinematicEvent)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * removes the first occurrence found of the given cinematicEvent for the
+     * given time stamp.
+     *
+     * @param timeStamp the timestamp when the cinematicEvent has been added
+     * @param cinematicEvent the cinematicEvent to remove
+     * @return true if the element has been removed
+     */
+    public boolean removeCinematicEvent(float timeStamp, CinematicEvent cinematicEvent) {
+        cinematicEvent.dispose();
+        KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp);
+        return removeCinematicEvent(keyFrame, cinematicEvent);
+    }
+
+    /**
+     * removes the first occurrence found of the given cinematicEvent for the
+     * given keyFrame
+     *
+     * @param keyFrame the keyFrame returned by the addCinematicEvent method.
+     * @param cinematicEvent the cinematicEvent to remove
+     * @return true if the element has been removed
+     */
+    public boolean removeCinematicEvent(KeyFrame keyFrame, CinematicEvent cinematicEvent) {
+        cinematicEvent.dispose();
+        boolean ret = keyFrame.cinematicEvents.remove(cinematicEvent);
+        cinematicEvents.remove(cinematicEvent);
+        if (keyFrame.isEmpty()) {
+            timeLine.removeKeyFrame(keyFrame.getIndex());
+        }
+        return ret;
+    }
+
+    /**
+     * called internally
+     *
+     * @see AppState#render(com.jme3.renderer.RenderManager)
+     */
+    public void render(RenderManager rm) {
+    }
+
+    /**
+     * called internally
+     *
+     * @see AppState#postRender()
+     */
+    public void postRender() {
+    }
+
+    /**
+     * called internally
+     *
+     * @see AppState#cleanup()
+     */
+    public void cleanup() {
+    }
+
+    /**
+     * fits the duration of the cinematic to the duration of all its child
+     * cinematic events
+     */
+    public void fitDuration() {
+        KeyFrame kf = timeLine.getKeyFrameAtIndex(timeLine.getLastKeyFrameIndex());
+        float d = 0;
+        for (int i = 0; i < kf.getCinematicEvents().size(); i++) {
+            CinematicEvent ce = kf.getCinematicEvents().get(i);
+            float dur = timeLine.getKeyFrameTime(kf) + ce.getDuration() * ce.getSpeed();
+            if (d < dur) {
+                d = dur;
+            }
+        }
+
+        initialDuration = d;
+    }
+
+    /**
+     * Binds a camera to this cinematic, tagged by a unique name. This methods
+     * creates and returns a CameraNode for the cam and attach it to the scene.
+     * The control direction is set to SpatialToCamera. This camera Node can
+     * then be used in other events to handle the camera movements during the
+     * playback
+     *
+     * @param cameraName the unique tag the camera should have
+     * @param cam the scene camera.
+     * @return the created CameraNode.
+     */
+    public CameraNode bindCamera(String cameraName, Camera cam) {
+        if (cameras.containsKey(cameraName)) {
+            throw new IllegalArgumentException("Camera " + cameraName + " is already binded to this cinematic");
+        }
+        CameraNode node = new CameraNode(cameraName, cam);
+        node.setControlDir(ControlDirection.SpatialToCamera);
+        node.getControl(CameraControl.class).setEnabled(false);
+        cameras.put(cameraName, node);
+        scene.attachChild(node);
+        return node;
+    }
+
+    /**
+     * returns a cameraNode given its name
+     *
+     * @param cameraName the camera name (as registered in
+     * Cinematic#bindCamera())
+     * @return the cameraNode for this name
+     */
+    public CameraNode getCamera(String cameraName) {
+        return cameras.get(cameraName);
+    }
+
+    /**
+     * enable/disable the camera control of the cameraNode of the current cam
+     *
+     * @param enabled
+     */
+    private void setEnableCurrentCam(boolean enabled) {
+        if (currentCam != null) {
+            currentCam.getControl(CameraControl.class).setEnabled(enabled);
+        }
+    }
+
+    /**
+     * Sets the active camera instantly (use activateCamera if you want to
+     * schedule that event)
+     *
+     * @param cameraName the camera name (as registered in
+     * Cinematic#bindCamera())
+     */
+    public void setActiveCamera(String cameraName) {
+        setEnableCurrentCam(false);
+        currentCam = cameras.get(cameraName);
+        if (currentCam == null) {
+            logger.log(Level.WARNING, "{0} is not a camera bond to the cinematic, cannot activate", cameraName);
+        }
+        setEnableCurrentCam(true);
+    }
+
+    /**
+     * schedule an event that will activate the camera at the given time
+     *
+     * @param timeStamp the time to activate the cam
+     * @param cameraName the camera name (as registered in
+     * Cinematic#bindCamera())
+     */
+    public void activateCamera(final float timeStamp, final String cameraName) {
+        addCinematicEvent(timeStamp, new CameraEvent(this, cameraName));
+    }
+
+    /**
+     * returns the complete eventdata map
+     *
+     * @return the eventdata map
+     */
+    private Map<String, Map<Object, Object>> getEventsData() {
+        if (eventsData == null) {
+            eventsData = new HashMap<String, Map<Object, Object>>();
+        }
+        return eventsData;
+    }
+
+    /**
+     * used internally put an eventdata in the cinematic
+     *
+     * @param type the type of data
+     * @param key the key
+     * @param object the data
+     */
+    public void putEventData(String type, Object key, Object object) {
+        Map<String, Map<Object, Object>> data = getEventsData();
+        Map<Object, Object> row = data.get(type);
+        if (row == null) {
+            row = new HashMap<Object, Object>();
+        }
+        row.put(key, object);
+        data.put(type, row);
+    }
+
+    /**
+     * used internally return and event data
+     *
+     * @param type the type of data
+     * @param key the key
+     * @return
+     */
+    public Object getEventData(String type, Object key) {
+        if (eventsData != null) {
+            Map<Object, Object> row = eventsData.get(type);
+            if (row != null) {
+                return row.get(key);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Used internally remove an eventData
+     *
+     * @param type the type of data
+     * @param key the key of the data
+     */
+    public void removeEventData(String type, Object key) {
+        if (eventsData != null) {
+            Map<Object, Object> row = eventsData.get(type);
+            if (row != null) {
+                row.remove(key);
+            }
+        }
+    }
+
+    /**
+     * sets the scene to use for this cinematic it is expected that the scene is
+     * added before adding events to the cinematic
+     *
+     * @param scene the scene where the cinematic should take place.
+     */
+    public void setScene(Node scene) {
+        this.scene = scene;
+    }
+
+    /**
+     * return the scene where the cinematic occur
+     *
+     * @return the scene
+     */
+    public Node getScene() {
+        return scene;
+    }
+
+    /**
+     * clear the cinematic of its events.
+     */
+    public void clear() {
+        dispose();
+        cinematicEvents.clear();
+        timeLine.clear();
+        if (eventsData != null) {
+            eventsData.clear();
+        }
+    }
+
+    /**
+     * used internally to cleanup the cinematic. Called when the clear() method
+     * is called
+     */
+    @Override
+    public void dispose() {
+        for (CinematicEvent event : cinematicEvents) {
+            event.dispose();
+        }
+    }
+}

+ 93 - 89
jme3-core/src/main/java/com/jme3/cinematic/KeyFrame.java

@@ -1,89 +1,93 @@
-/*
- * Copyright (c) 2009-2012 jMonkeyEngine
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * * Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- *
- * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
- *   may be used to endorse or promote products derived from this software
- *   without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.jme3.cinematic;
-
-import com.jme3.cinematic.events.CinematicEvent;
-import com.jme3.export.*;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- *
- * @author Nehon
- */
-public class KeyFrame implements Savable {
-
-    List<CinematicEvent> cinematicEvents = new ArrayList<CinematicEvent>();
-    private int index;
-
-    public List<CinematicEvent> getCinematicEvents() {
-        return cinematicEvents;
-    }
-
-    public void setCinematicEvents(List<CinematicEvent> cinematicEvents) {
-        this.cinematicEvents = cinematicEvents;
-    }
-
-    public List<CinematicEvent> trigger() {
-        for (CinematicEvent event : cinematicEvents) {
-            event.play();
-        }
-        return cinematicEvents;
-    }
-    
-    public boolean isEmpty(){
-        return cinematicEvents.isEmpty();
-    }
-
-    public void write(JmeExporter ex) throws IOException {
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.writeSavableArrayList((ArrayList) cinematicEvents, "cinematicEvents", null);
-        oc.write(index, "index", 0);
-    }
-
-    public void read(JmeImporter im) throws IOException {
-        InputCapsule ic = im.getCapsule(this);
-        cinematicEvents = ic.readSavableArrayList("cinematicEvents", null);
-        index=ic.readInt("index", 0);
-    }
-
-    public int getIndex() {
-        return index;
-    }
-
-    public void setIndex(int index) {
-        this.index = index;
-    }
-
-
-}
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.cinematic;
+
+import com.jme3.cinematic.events.CinematicEvent;
+import com.jme3.export.*;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ *
+ * @author Nehon
+ */
+public class KeyFrame implements Savable {
+
+    public KeyFrame(){
+        
+    }
+    
+    List<CinematicEvent> cinematicEvents = new ArrayList<>();
+    private int index;
+
+    public List<CinematicEvent> getCinematicEvents() {
+        return cinematicEvents;
+    }
+
+    public void setCinematicEvents(List<CinematicEvent> cinematicEvents) {
+        this.cinematicEvents = cinematicEvents;
+    }
+
+    public List<CinematicEvent> trigger() {
+        for (CinematicEvent event : cinematicEvents) {
+            event.play();
+        }
+        return cinematicEvents;
+    }
+    
+    public boolean isEmpty(){
+        return cinematicEvents.isEmpty();
+    }
+
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.writeSavableArrayList((ArrayList) cinematicEvents, "cinematicEvents", null);
+        oc.write(index, "index", 0);
+    }
+
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule ic = im.getCapsule(this);
+        cinematicEvents = ic.readSavableArrayList("cinematicEvents", null);
+        index=ic.readInt("index", 0);
+    }
+
+    public int getIndex() {
+        return index;
+    }
+
+    public void setIndex(int index) {
+        this.index = index;
+    }
+
+
+}

+ 448 - 447
jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java

@@ -1,447 +1,448 @@
-/*
- * Copyright (c) 2009-2012 jMonkeyEngine
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * * Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- *
- * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
- *   may be used to endorse or promote products derived from this software
- *   without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.jme3.cinematic.events;
-
-import com.jme3.animation.AnimChannel;
-import com.jme3.animation.AnimControl;
-import com.jme3.animation.LoopMode;
-import com.jme3.app.Application;
-import com.jme3.cinematic.Cinematic;
-import com.jme3.cinematic.PlayState;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import com.jme3.scene.Spatial;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.logging.Logger;
-
-/**
- * An event based on an animation of a model. The model has to hold an
- * AnimControl with valid animation (bone or spatial animations).
- *
- * It helps to schedule the playback of an animation on a model in a Cinematic.
- *
- *
- * @author Nehon
- */
-public class AnimationEvent extends AbstractCinematicEvent {
-
-    // Version #2: directly keeping track on the model instead of trying to retrieve 
-    //it from the scene according to its name, because the name is not supposed to be unique
-    //For backward compatibility, if the model is null it's looked up into the scene
-    public static final int SAVABLE_VERSION = 2;
-    private static final Logger log = Logger.getLogger(AnimationEvent.class.getName());
-    public static final String MODEL_CHANNELS = "modelChannels";
-    protected AnimChannel channel;
-    protected String animationName;
-    protected Spatial model;
-    //kept for backward compatibility
-    protected String modelName;
-    protected float blendTime = 0;
-    protected int channelIndex = 0;
-    // parent cinematic
-    protected Cinematic cinematic;
-
-    /**
-     * used for serialization don't call directly use one of the following
-     * constructors
-     */
-    public AnimationEvent() {
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     */
-    public AnimationEvent(Spatial model, String animationName) {
-        this.model = model;
-        this.animationName = animationName;
-        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param initialDuration the initial duration of the event
-     */
-    public AnimationEvent(Spatial model, String animationName, float initialDuration) {
-        super(initialDuration);
-        this.model = model;
-        this.animationName = animationName;
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param loopMode the loopMode
-     * @see LoopMode
-     */
-    public AnimationEvent(Spatial model, String animationName, LoopMode loopMode) {
-        super(loopMode);
-        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
-        this.model = model;
-        this.animationName = animationName;
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param initialDuration the initial duration of the event
-     * @param loopMode the loopMode
-     * @see LoopMode
-     */
-    public AnimationEvent(Spatial model, String animationName, float initialDuration, LoopMode loopMode) {
-        super(initialDuration, loopMode);
-        this.model = model;
-        this.animationName = animationName;
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param initialDuration the initial duration of the event
-     * @param blendTime the time during the animation are gonna be blended
-     * @see AnimChannel#setAnim(java.lang.String, float)
-     */
-    public AnimationEvent(Spatial model, String animationName, float initialDuration, float blendTime) {
-        super(initialDuration);
-        this.model = model;
-        this.animationName = animationName;
-        this.blendTime = blendTime;
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param loopMode the loopMode
-     * @see LoopMode
-     * @param blendTime the time during the animation are gonna be blended
-     * @see AnimChannel#setAnim(java.lang.String, float)
-     */
-    public AnimationEvent(Spatial model, String animationName, LoopMode loopMode, float blendTime) {
-        super(loopMode);
-        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
-        this.model = model;
-        this.animationName = animationName;
-        this.blendTime = blendTime;
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param initialDuration the initial duration of the event
-     * @param loopMode the loopMode
-     * @see LoopMode
-     * @param blendTime the time during the animation are gonna be blended
-     * @see AnimChannel#setAnim(java.lang.String, float)
-     */
-    public AnimationEvent(Spatial model, String animationName, float initialDuration, LoopMode loopMode, float blendTime) {
-        super(initialDuration, loopMode);
-        this.model = model;
-        this.animationName = animationName;
-        this.blendTime = blendTime;
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param loopMode the loopMode
-     * @see LoopMode
-     * @param channelIndex the index of the channel default is 0. Events on the
-     * same channelIndex will use the same channel.
-     */
-    public AnimationEvent(Spatial model, String animationName, LoopMode loopMode, int channelIndex) {
-        super(loopMode);
-        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
-        this.model = model;
-        this.animationName = animationName;
-        this.channelIndex = channelIndex;
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param channelIndex the index of the channel default is 0. Events on the
-     * same channelIndex will use the same channel.
-     */
-    public AnimationEvent(Spatial model, String animationName, int channelIndex) {
-        this.model = model;
-        this.animationName = animationName;
-        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
-        this.channelIndex = channelIndex;
-    }
-    
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param channelIndex the index of the channel default is 0. Events on the
-     * @param blendTime the time during the animation are gonna be blended
-     * same channelIndex will use the same channel.
-     */
-    public AnimationEvent(Spatial model, String animationName, LoopMode loopMode, int channelIndex, float blendTime) {
-        this.model = model;
-        this.animationName = animationName;
-        this.loopMode = loopMode;
-        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
-        this.channelIndex = channelIndex;
-        this.blendTime = blendTime;
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param initialDuration the initial duration of the event
-     * @param channelIndex the index of the channel default is 0. Events on the
-     * same channelIndex will use the same channel.
-     */
-    public AnimationEvent(Spatial model, String animationName, float initialDuration, int channelIndex) {
-        super(initialDuration);
-        this.model = model;
-        this.animationName = animationName;
-        this.channelIndex = channelIndex;
-    }
-
-    /**
-     * creates an animation event
-     *
-     * @param model the model on which the animation will be played
-     * @param animationName the name of the animation to play
-     * @param initialDuration the initial duration of the event
-     * @param loopMode the loopMode
-     * @see LoopMode
-     * @param channelIndex the index of the channel default is 0. Events on the
-     * same channelIndex will use the same channel.
-     */
-    public AnimationEvent(Spatial model, String animationName, float initialDuration, LoopMode loopMode, int channelIndex) {
-        super(initialDuration, loopMode);
-        this.model = model;
-        this.animationName = animationName;
-        this.channelIndex = channelIndex;
-    }
-
-    @Override
-    public void initEvent(Application app, Cinematic cinematic) {
-        super.initEvent(app, cinematic);
-        this.cinematic = cinematic;
-        if (channel == null) {
-            Object s = cinematic.getEventData(MODEL_CHANNELS, model);
-            if (s == null) {
-                s = new HashMap<Integer, AnimChannel>();
-                int numChannels = model.getControl(AnimControl.class).getNumChannels();
-                for(int i = 0; i < numChannels; i++){
-                    ((HashMap<Integer, AnimChannel>)s).put(i, model.getControl(AnimControl.class).getChannel(i));
-                }
-                cinematic.putEventData(MODEL_CHANNELS, model, s);
-            }
-
-            Map<Integer, AnimChannel> map = (Map<Integer, AnimChannel>) s;
-            this.channel = map.get(channelIndex);
-            if (this.channel == null) {
-                if (model == null) {
-                    //the model is null we try to find it according to the name
-                    //this should occur only when loading an old saved cinematic
-                    //othewise it's an error
-                    model = cinematic.getScene().getChild(modelName);
-                }
-                if (model != null) {
-                    channel = model.getControl(AnimControl.class).createChannel();
-                    map.put(channelIndex, channel);
-                } else {
-                    //it's an error
-                    throw new UnsupportedOperationException("model should not be null");
-                }
-            } 
-
-        }
-    }
-
-    @Override
-    public void setTime(float time) {
-        super.setTime(time);
-        if (!animationName.equals(channel.getAnimationName())) {
-            channel.setAnim(animationName, blendTime);
-        }
-        float t = time;
-        if (loopMode == loopMode.Loop) {
-            t = t % channel.getAnimMaxTime();
-        }
-        if (loopMode == loopMode.Cycle) {
-            float parity = (float) Math.ceil(time / channel.getAnimMaxTime());
-            if (parity > 0 && parity % 2 == 0) {
-                t = channel.getAnimMaxTime() - t % channel.getAnimMaxTime();
-            } else {
-                t = t % channel.getAnimMaxTime();
-            }
-
-        }
-        if (t < 0) {
-            channel.setTime(0);
-            channel.reset(true);
-        }
-        if (t > channel.getAnimMaxTime()) {
-            channel.setTime(t);
-            channel.getControl().update(0);
-            stop();
-        } else {
-            channel.setTime(t);
-            channel.getControl().update(0);
-        }
-
-    }
-
-    @Override
-    public void onPlay() {
-        channel.getControl().setEnabled(true);
-        if (playState == PlayState.Stopped) {
-            channel.setAnim(animationName, blendTime);
-            channel.setSpeed(speed);
-            channel.setLoopMode(loopMode);
-            channel.setTime(0);
-        }
-    }
-
-    @Override
-    public void setSpeed(float speed) {
-        super.setSpeed(speed);
-        if (channel != null) {
-            channel.setSpeed(speed);
-        }
-    }
-
-    @Override
-    public void onUpdate(float tpf) {
-    }
-
-    @Override
-    public void onStop() {
-    }
-
-    @Override
-    public void forceStop() {
-        if (channel != null) {
-            channel.setTime(time);
-            channel.reset(false);
-        }
-        super.forceStop();
-    }
-
-    @Override
-    public void onPause() {
-        if (channel != null) {
-            channel.getControl().setEnabled(false);
-        }
-    }
-
-    @Override
-    public void setLoopMode(LoopMode loopMode) {
-        super.setLoopMode(loopMode);
-        if (channel != null) {
-            channel.setLoopMode(loopMode);
-        }
-    }
-
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-
-        oc.write(model, "model", null);
-        oc.write(animationName, "animationName", "");
-        oc.write(blendTime, "blendTime", 0f);
-        oc.write(channelIndex, "channelIndex", 0);
-
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        InputCapsule ic = im.getCapsule(this);
-        if (im.getFormatVersion() == 0) {
-            modelName = ic.readString("modelName", "");
-        }
-        //FIXME always the same issue, because of the clonning of assets, this won't work
-        //we have to somehow store userdata in the spatial and then recurse the 
-        //scene sub scenegraph to find the correct instance of the model
-        //This brings a reflaxion about the cinematic being an appstate, 
-        //shouldn't it be a control over the scene
-        // this would allow to use the cloneForSpatial method and automatically 
-        //rebind cloned references of original objects.
-        //for now as nobody probably ever saved a cinematic, this is not a critical issue
-        model = (Spatial) ic.readSavable("model", null);
-        animationName = ic.readString("animationName", "");
-        blendTime = ic.readFloat("blendTime", 0f);
-        channelIndex = ic.readInt("channelIndex", 0);
-    }
-
-    @Override
-    public void dispose() {
-        super.dispose();
-        if (cinematic != null) {
-            Object o = cinematic.getEventData(MODEL_CHANNELS, model);
-            if (o != null) {
-                Collection<AnimChannel> values = ((HashMap<Integer, AnimChannel>) o).values();
-                while (values.remove(channel));
-                if (values.isEmpty()) {
-                    cinematic.removeEventData(MODEL_CHANNELS, model);
-                }
-            }
-            cinematic = null;
-            channel = null;
-        }
-    }
-}
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.cinematic.events;
+
+import com.jme3.animation.AnimChannel;
+import com.jme3.animation.AnimControl;
+import com.jme3.animation.LoopMode;
+import com.jme3.app.Application;
+import com.jme3.cinematic.Cinematic;
+import com.jme3.cinematic.PlayState;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.scene.Spatial;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+/**
+ * An event based on an animation of a model. The model has to hold an
+ * AnimControl with valid animation (bone or spatial animations).
+ *
+ * It helps to schedule the playback of an animation on a model in a Cinematic.
+ *
+ *
+ * @author Nehon
+ */
+public class AnimationEvent extends AbstractCinematicEvent {
+
+    // Version #2: directly keeping track on the model instead of trying to retrieve 
+    //it from the scene according to its name, because the name is not supposed to be unique
+    //For backward compatibility, if the model is null it's looked up into the scene
+    public static final int SAVABLE_VERSION = 2;
+    private static final Logger log = Logger.getLogger(AnimationEvent.class.getName());
+    public static final String MODEL_CHANNELS = "modelChannels";
+    protected AnimChannel channel;
+    protected String animationName;
+    protected Spatial model;
+    //kept for backward compatibility
+    protected String modelName;
+    protected float blendTime = 0;
+    protected int channelIndex = 0;
+    // parent cinematic
+    protected Cinematic cinematic;
+
+    /**
+     * used for serialization don't call directly use one of the following
+     * constructors
+     */
+    public AnimationEvent() {
+        super();
+    }
+    
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     */
+    public AnimationEvent(Spatial model, String animationName) {
+        this.model = model;
+        this.animationName = animationName;
+        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
+    }
+
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param initialDuration the initial duration of the event
+     */
+    public AnimationEvent(Spatial model, String animationName, float initialDuration) {
+        super(initialDuration);
+        this.model = model;
+        this.animationName = animationName;
+    }
+
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param loopMode the loopMode
+     * @see LoopMode
+     */
+    public AnimationEvent(Spatial model, String animationName, LoopMode loopMode) {
+        super(loopMode);
+        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
+        this.model = model;
+        this.animationName = animationName;
+    }
+
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param initialDuration the initial duration of the event
+     * @param loopMode the loopMode
+     * @see LoopMode
+     */
+    public AnimationEvent(Spatial model, String animationName, float initialDuration, LoopMode loopMode) {
+        super(initialDuration, loopMode);
+        this.model = model;
+        this.animationName = animationName;
+    }
+
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param initialDuration the initial duration of the event
+     * @param blendTime the time during the animation are gonna be blended
+     * @see AnimChannel#setAnim(java.lang.String, float)
+     */
+    public AnimationEvent(Spatial model, String animationName, float initialDuration, float blendTime) {
+        super(initialDuration);
+        this.model = model;
+        this.animationName = animationName;
+        this.blendTime = blendTime;
+    }
+
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param loopMode the loopMode
+     * @see LoopMode
+     * @param blendTime the time during the animation are gonna be blended
+     * @see AnimChannel#setAnim(java.lang.String, float)
+     */
+    public AnimationEvent(Spatial model, String animationName, LoopMode loopMode, float blendTime) {
+        super(loopMode);
+        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
+        this.model = model;
+        this.animationName = animationName;
+        this.blendTime = blendTime;
+    }
+
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param initialDuration the initial duration of the event
+     * @param loopMode the loopMode
+     * @see LoopMode
+     * @param blendTime the time during the animation are gonna be blended
+     * @see AnimChannel#setAnim(java.lang.String, float)
+     */
+    public AnimationEvent(Spatial model, String animationName, float initialDuration, LoopMode loopMode, float blendTime) {
+        super(initialDuration, loopMode);
+        this.model = model;
+        this.animationName = animationName;
+        this.blendTime = blendTime;
+    }
+
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param loopMode the loopMode
+     * @see LoopMode
+     * @param channelIndex the index of the channel default is 0. Events on the
+     * same channelIndex will use the same channel.
+     */
+    public AnimationEvent(Spatial model, String animationName, LoopMode loopMode, int channelIndex) {
+        super(loopMode);
+        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
+        this.model = model;
+        this.animationName = animationName;
+        this.channelIndex = channelIndex;
+    }
+
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param channelIndex the index of the channel default is 0. Events on the
+     * same channelIndex will use the same channel.
+     */
+    public AnimationEvent(Spatial model, String animationName, int channelIndex) {
+        this.model = model;
+        this.animationName = animationName;
+        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
+        this.channelIndex = channelIndex;
+    }
+    
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param channelIndex the index of the channel default is 0. Events on the
+     * @param blendTime the time during the animation are gonna be blended
+     * same channelIndex will use the same channel.
+     */
+    public AnimationEvent(Spatial model, String animationName, LoopMode loopMode, int channelIndex, float blendTime) {
+        this.model = model;
+        this.animationName = animationName;
+        this.loopMode = loopMode;
+        initialDuration = model.getControl(AnimControl.class).getAnimationLength(animationName);
+        this.channelIndex = channelIndex;
+        this.blendTime = blendTime;
+    }
+
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param initialDuration the initial duration of the event
+     * @param channelIndex the index of the channel default is 0. Events on the
+     * same channelIndex will use the same channel.
+     */
+    public AnimationEvent(Spatial model, String animationName, float initialDuration, int channelIndex) {
+        super(initialDuration);
+        this.model = model;
+        this.animationName = animationName;
+        this.channelIndex = channelIndex;
+    }
+
+    /**
+     * creates an animation event
+     *
+     * @param model the model on which the animation will be played
+     * @param animationName the name of the animation to play
+     * @param initialDuration the initial duration of the event
+     * @param loopMode the loopMode
+     * @see LoopMode
+     * @param channelIndex the index of the channel default is 0. Events on the
+     * same channelIndex will use the same channel.
+     */
+    public AnimationEvent(Spatial model, String animationName, float initialDuration, LoopMode loopMode, int channelIndex) {
+        super(initialDuration, loopMode);
+        this.model = model;
+        this.animationName = animationName;
+        this.channelIndex = channelIndex;
+    }
+
+    @Override
+    public void initEvent(Application app, Cinematic cinematic) {
+        super.initEvent(app, cinematic);
+        this.cinematic = cinematic;
+        if (channel == null) {
+            Object s = cinematic.getEventData(MODEL_CHANNELS, model);
+            if (s == null) {
+                s = new HashMap<Integer, AnimChannel>();
+                int numChannels = model.getControl(AnimControl.class).getNumChannels();
+                for(int i = 0; i < numChannels; i++){
+                    ((HashMap<Integer, AnimChannel>)s).put(i, model.getControl(AnimControl.class).getChannel(i));
+                }
+                cinematic.putEventData(MODEL_CHANNELS, model, s);
+            }
+
+            Map<Integer, AnimChannel> map = (Map<Integer, AnimChannel>) s;
+            this.channel = map.get(channelIndex);
+            if (this.channel == null) {
+                if (model == null) {
+                    //the model is null we try to find it according to the name
+                    //this should occur only when loading an old saved cinematic
+                    //othewise it's an error
+                    model = cinematic.getScene().getChild(modelName);
+                }
+                if (model != null) {
+                    channel = model.getControl(AnimControl.class).createChannel();
+                    map.put(channelIndex, channel);
+                } else {
+                    //it's an error
+                    throw new UnsupportedOperationException("model should not be null");
+                }
+            } 
+
+        }
+    }
+
+    @Override
+    public void setTime(float time) {
+        super.setTime(time);
+        if (!animationName.equals(channel.getAnimationName())) {
+            channel.setAnim(animationName, blendTime);
+        }
+        float t = time;
+        if (loopMode == loopMode.Loop) {
+            t = t % channel.getAnimMaxTime();
+        }
+        if (loopMode == loopMode.Cycle) {
+            float parity = (float) Math.ceil(time / channel.getAnimMaxTime());
+            if (parity > 0 && parity % 2 == 0) {
+                t = channel.getAnimMaxTime() - t % channel.getAnimMaxTime();
+            } else {
+                t = t % channel.getAnimMaxTime();
+            }
+
+        }
+        if (t < 0) {
+            channel.setTime(0);
+            channel.reset(true);
+        }
+        if (t > channel.getAnimMaxTime()) {
+            channel.setTime(t);
+            channel.getControl().update(0);
+            stop();
+        } else {
+            channel.setTime(t);
+            channel.getControl().update(0);
+        }
+
+    }
+
+    @Override
+    public void onPlay() {
+        channel.getControl().setEnabled(true);
+        if (playState == PlayState.Stopped) {
+            channel.setAnim(animationName, blendTime);
+            channel.setSpeed(speed);
+            channel.setLoopMode(loopMode);
+            channel.setTime(0);
+        }
+    }
+
+    @Override
+    public void setSpeed(float speed) {
+        super.setSpeed(speed);
+        if (channel != null) {
+            channel.setSpeed(speed);
+        }
+    }
+
+    @Override
+    public void onUpdate(float tpf) {
+    }
+
+    @Override
+    public void onStop() {
+    }
+
+    @Override
+    public void forceStop() {
+        if (channel != null) {
+            channel.setTime(time);
+            channel.reset(false);
+        }
+        super.forceStop();
+    }
+
+    @Override
+    public void onPause() {
+        if (channel != null) {
+            channel.getControl().setEnabled(false);
+        }
+    }
+
+    @Override
+    public void setLoopMode(LoopMode loopMode) {
+        super.setLoopMode(loopMode);
+        if (channel != null) {
+            channel.setLoopMode(loopMode);
+        }
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+
+        oc.write(model, "model", null);
+        oc.write(animationName, "animationName", "");
+        oc.write(blendTime, "blendTime", 0f);
+        oc.write(channelIndex, "channelIndex", 0);
+
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        if (im.getFormatVersion() == 0) {
+            modelName = ic.readString("modelName", "");
+        }
+        //FIXME always the same issue, because of the clonning of assets, this won't work
+        //we have to somehow store userdata in the spatial and then recurse the 
+        //scene sub scenegraph to find the correct instance of the model
+        //This brings a reflaxion about the cinematic being an appstate, 
+        //shouldn't it be a control over the scene
+        // this would allow to use the cloneForSpatial method and automatically 
+        //rebind cloned references of original objects.
+        //for now as nobody probably ever saved a cinematic, this is not a critical issue
+        model = (Spatial) ic.readSavable("model", null);
+        animationName = ic.readString("animationName", "");
+        blendTime = ic.readFloat("blendTime", 0f);
+        channelIndex = ic.readInt("channelIndex", 0);
+    }
+
+    @Override
+    public void dispose() {
+        super.dispose();
+        if (cinematic != null) {
+            Object o = cinematic.getEventData(MODEL_CHANNELS, model);
+            if (o != null) {
+                Collection<AnimChannel> values = ((HashMap<Integer, AnimChannel>) o).values();
+                while (values.remove(channel));
+                if (values.isEmpty()) {
+                    cinematic.removeEventData(MODEL_CHANNELS, model);
+                }
+            }
+            cinematic = null;
+            channel = null;
+        }
+    }
+}

+ 146 - 0
jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java

@@ -0,0 +1,146 @@
+/*
+ * Copyright (c) 2009-2017 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.cinematic.events;
+
+import com.jme3.app.Application;
+import com.jme3.cinematic.Cinematic;
+import com.jme3.cinematic.TimeLine;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
+import com.jme3.scene.CameraNode;
+import java.io.IOException;
+import java.util.Map;
+
+/**
+ *
+ * @author Rickard <neph1 @ github>
+ */
+public class CameraEvent extends AbstractCinematicEvent{
+
+    private String cameraName;
+    private Cinematic cinematic;
+
+    public String getCameraName() {
+        return cameraName;
+    }
+
+    public void setCameraName(String cameraName) {
+        this.cameraName = cameraName;
+    }
+
+    public CameraEvent(){
+        
+    }
+    
+    public CameraEvent(Cinematic parentEvent, String cameraName){
+        this.cinematic = parentEvent;
+        this.cameraName = cameraName;
+    }
+    
+     @Override
+    public void initEvent(Application app, Cinematic cinematic) {
+        super.initEvent(app, cinematic);
+        this.cinematic = cinematic;
+    }
+    
+    @Override
+    public void play() {
+        super.play();
+        stop();
+    }
+    
+    @Override
+    public void onPlay() {
+        cinematic.setActiveCamera(cameraName);
+    }
+
+    @Override
+    public void onUpdate(float tpf) {
+    }
+
+    @Override
+    public void onStop() {
+    }
+
+    @Override
+    public void onPause() {
+    }
+
+    @Override
+    public void forceStop() {
+    }
+
+    @Override
+    public void setTime(float time) {
+        play();
+    }
+
+    public Cinematic getCinematic() {
+        return cinematic;
+    }
+
+    public void setCinematic(Cinematic cinematic) {
+        this.cinematic = cinematic;
+    }
+    
+    
+    
+    /**
+     * used internally for serialization
+     *
+     * @param ex
+     * @throws IOException
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(cameraName, "cameraName", null);
+
+    }
+
+    /**
+     * used internally for serialization
+     *
+     * @param im
+     * @throws IOException
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        cameraName = ic.readString("cameraName", null);
+    }
+}

+ 491 - 491
jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java

@@ -1,491 +1,491 @@
-/*
- * Copyright (c) 2009-2016 jMonkeyEngine
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * * Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- *
- * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
- *   may be used to endorse or promote products derived from this software
- *   without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.jme3.cinematic.events;
-
-import com.jme3.animation.AnimationUtils;
-import com.jme3.animation.LoopMode;
-import com.jme3.app.Application;
-import com.jme3.cinematic.Cinematic;
-import com.jme3.cinematic.MotionPath;
-import com.jme3.cinematic.PlayState;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import com.jme3.math.Quaternion;
-import com.jme3.math.Vector3f;
-import com.jme3.renderer.RenderManager;
-import com.jme3.renderer.ViewPort;
-import com.jme3.scene.Spatial;
-import com.jme3.scene.control.Control;
-import com.jme3.util.clone.Cloner;
-import com.jme3.util.clone.JmeCloneable;
-import java.io.IOException;
-
-/**
- * A MotionEvent is a control over the spatial that manages the position and direction of the spatial while following a motion Path.
- *
- * You must first create a MotionPath and then create a MotionEvent to associate a spatial and the path.
- *
- * @author Nehon
- */
-public class MotionEvent extends AbstractCinematicEvent implements Control, JmeCloneable {
-
-    protected Spatial spatial;
-    protected int currentWayPoint;
-    protected float currentValue;
-    protected Vector3f direction = new Vector3f();
-    protected Vector3f lookAt = null;
-    protected Vector3f upVector = Vector3f.UNIT_Y;
-    protected Quaternion rotation = null;
-    protected Direction directionType = Direction.None;
-    protected MotionPath path;
-    private boolean isControl = true;
-    private int travelDirection = 1;
-    /**
-     * the distance traveled by the spatial on the path
-     */
-    protected float traveledDistance = 0;
-
-    /**
-     * Enum for the different type of target direction behavior.
-     */
-    public enum Direction {
-
-        /**
-         * The target stays in the starting direction.
-         */
-        None,
-        /**
-         * The target rotates with the direction of the path.
-         */
-        Path,
-        /**
-         * The target rotates with the direction of the path but with the addition of a rotation.
-         * You need to use the setRotation method when using this Direction.
-         */
-        PathAndRotation,
-        /**
-         * The target rotates with the given rotation.
-         */
-        Rotation,
-        /**
-         * The target looks at a point.
-         * You need to use the setLookAt method when using this direction.
-         */
-        LookAt
-    }
-
-    /**
-     * Create MotionEvent,
-     * when using this constructor don't forget to assign spatial and path.
-     */
-    public MotionEvent() {
-        super();
-    }
-
-    /**
-     * Creates a MotionPath for the given spatial on the given motion path.
-     * @param spatial
-     * @param path
-     */
-    public MotionEvent(Spatial spatial, MotionPath path) {
-        super();
-        spatial.addControl(this);
-        this.path = path;
-    }
-
-    /**
-     * Creates a MotionPath for the given spatial on the given motion path.
-     * @param spatial
-     * @param path
-     */
-    public MotionEvent(Spatial spatial, MotionPath path, float initialDuration) {
-        super(initialDuration);
-        spatial.addControl(this);
-        this.path = path;
-    }
-
-    /**
-     * Creates a MotionPath for the given spatial on the given motion path.
-     * @param spatial
-     * @param path
-     */
-    public MotionEvent(Spatial spatial, MotionPath path, LoopMode loopMode) {
-        super();
-        spatial.addControl(this);
-        this.path = path;
-        this.loopMode = loopMode;
-    }
-
-    /**
-     * Creates a MotionPath for the given spatial on the given motion path.
-     * @param spatial
-     * @param path
-     */
-    public MotionEvent(Spatial spatial, MotionPath path, float initialDuration, LoopMode loopMode) {
-        super(initialDuration);
-        spatial.addControl(this);
-        this.path = path;
-        this.loopMode = loopMode;
-    }
-
-    public void update(float tpf) {
-        if (isControl) {
-            internalUpdate(tpf);
-        }
-    }
-
-    @Override
-    public void internalUpdate(float tpf) {
-        if (playState == PlayState.Playing) {
-            time = time + (tpf * speed);
-            if (loopMode == LoopMode.Loop && time < 0) {
-                time = initialDuration;
-            }            
-            if ((time >= initialDuration || time < 0) && loopMode == LoopMode.DontLoop) {
-                if (time >= initialDuration) {
-                    path.triggerWayPointReach(path.getNbWayPoints() - 1, this);
-                }
-                stop();
-            } else {
-                time = AnimationUtils.clampWrapTime(time, initialDuration, loopMode);
-                if(time<0){
-                    speed = - speed;
-                    time = - time;
-                }
-                onUpdate(tpf);
-            }
-        }
-    }
-
-    @Override
-    public void initEvent(Application app, Cinematic cinematic) {
-        super.initEvent(app, cinematic);
-        isControl = false;
-    }
-
-    @Override
-    public void setTime(float time) {
-        super.setTime(time);
-        onUpdate(0);
-    }
-
-    public void onUpdate(float tpf) {
-        traveledDistance = path.interpolatePath(time, this, tpf);
-        computeTargetDirection();
-    }
-
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(lookAt, "lookAt", null);
-        oc.write(upVector, "upVector", Vector3f.UNIT_Y);
-        oc.write(rotation, "rotation", null);
-        oc.write(directionType, "directionType", Direction.None);
-        oc.write(path, "path", null);
-        oc.write(spatial, "spatial", null);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        InputCapsule in = im.getCapsule(this);
-        lookAt = (Vector3f) in.readSavable("lookAt", null);
-        upVector = (Vector3f) in.readSavable("upVector", Vector3f.UNIT_Y);
-        rotation = (Quaternion) in.readSavable("rotation", null);
-        directionType = in.readEnum("directionType", Direction.class, Direction.None);
-        path = (MotionPath) in.readSavable("path", null);
-        spatial = (Spatial) in.readSavable("spatial", null);
-    }
-
-    /**
-     * This method is meant to be called by the motion path only.
-     * @return
-     */
-    public boolean needsDirection() {
-        return directionType == Direction.Path || directionType == Direction.PathAndRotation;
-    }
-
-    private void computeTargetDirection() {
-        switch (directionType) {
-            case Path:
-                Quaternion q = new Quaternion();
-                q.lookAt(direction, upVector);
-                spatial.setLocalRotation(q);
-                break;
-            case LookAt:
-                if (lookAt != null) {
-                    spatial.lookAt(lookAt, upVector);
-                }
-                break;
-            case PathAndRotation:
-                if (rotation != null) {
-                    Quaternion q2 = new Quaternion();
-                    q2.lookAt(direction, upVector);
-                    q2.multLocal(rotation);
-                    spatial.setLocalRotation(q2);
-                }
-                break;
-            case Rotation:
-                if (rotation != null) {
-                    spatial.setLocalRotation(rotation);
-                }
-                break;
-            case None:
-                break;
-            default:
-                break;
-        }
-    }
-
-    /**
-     * Clone this control for the given spatial.
-     * @param spatial
-     * @return
-     */
-    @Override
-    public Control cloneForSpatial(Spatial spatial) {
-        MotionEvent control = new MotionEvent();
-        control.setPath(path);
-        control.playState = playState;
-        control.currentWayPoint = currentWayPoint;
-        control.currentValue = currentValue;
-        control.direction = direction.clone();
-        control.lookAt = lookAt;
-        control.upVector = upVector.clone();
-        control.rotation = rotation;
-        control.initialDuration = initialDuration;
-        control.speed = speed;
-        control.loopMode = loopMode;
-        control.directionType = directionType;
-
-        return control;
-    }
-
-    @Override   
-    public Object jmeClone() {
-        MotionEvent control = new MotionEvent();
-        control.path = path;
-        control.playState = playState;
-        control.currentWayPoint = currentWayPoint;
-        control.currentValue = currentValue;
-        control.direction = direction.clone();
-        control.lookAt = lookAt;
-        control.upVector = upVector.clone();
-        control.rotation = rotation;
-        control.initialDuration = initialDuration;
-        control.speed = speed;
-        control.loopMode = loopMode;
-        control.directionType = directionType;
-        control.spatial = spatial;
-
-        return control;
-    }     
-
-    @Override   
-    public void cloneFields( Cloner cloner, Object original ) { 
-        this.spatial = cloner.clone(spatial);
-    }
-         
-    @Override
-    public void onPlay() {
-        traveledDistance = 0;
-    }
-
-    @Override
-    public void onStop() {
-        currentWayPoint = 0;
-    }
-
-    @Override
-    public void onPause() {
-    }
-
-    /**
-     * This method is meant to be called by the motion path only.
-     * @return
-     */
-    public float getCurrentValue() {
-        return currentValue;
-    }
-
-    /**
-     * This method is meant to be called by the motion path only.
-     *
-     */
-    public void setCurrentValue(float currentValue) {
-        this.currentValue = currentValue;
-    }
-
-    /**
-     * This method is meant to be called by the motion path only.
-     * @return
-     */
-    public int getCurrentWayPoint() {
-        return currentWayPoint;
-    }
-
-    /**
-     * This method is meant to be called by the motion path only.
-     *
-     */
-    public void setCurrentWayPoint(int currentWayPoint) {
-        this.currentWayPoint = currentWayPoint;
-    }
-
-    /**
-     * Returns the direction the spatial is moving.
-     * @return
-     */
-    public Vector3f getDirection() {
-        return direction;
-    }
-
-    /**
-     * Sets the direction of the spatial, using the Y axis as the up vector.
-     * Use MotionEvent#setDirection((Vector3f direction,Vector3f upVector) if 
-     * you want a custum up vector.
-     * This method is used by the motion path.
-     * @param direction
-     */
-    public void setDirection(Vector3f direction) {
-        setDirection(direction, Vector3f.UNIT_Y); 
-   }
-    
-    /**
-     * Sets the direction of the spatial with the given up vector.
-     * This method is used by the motion path.
-     * @param direction
-     * @param upVector the up vector to consider for this direction.
-     */
-    public void setDirection(Vector3f direction,Vector3f upVector) {
-        this.direction.set(direction);
-        this.upVector.set(upVector);
-    }
-
-    /**
-     * Returns the direction type of the target.
-     * @return the direction type.
-     */
-    public Direction getDirectionType() {
-        return directionType;
-    }
-
-    /**
-     * Sets the direction type of the target.
-     * On each update the direction given to the target can have different behavior.
-     * See the Direction Enum for explanations.
-     * @param directionType the direction type.
-     */
-    public void setDirectionType(Direction directionType) {
-        this.directionType = directionType;
-    }
-
-    /**
-     * Set the lookAt for the target.
-     * This can be used only if direction Type is Direction.LookAt.
-     * @param lookAt the position to look at.
-     * @param upVector the up vector.
-     */
-    public void setLookAt(Vector3f lookAt, Vector3f upVector) {
-        this.lookAt = lookAt;
-        this.upVector = upVector;
-    }
-
-    /**
-     * Returns the rotation of the target.
-     * @return the rotation quaternion.
-     */
-    public Quaternion getRotation() {
-        return rotation;
-    }
-
-    /**
-     * Sets the rotation of the target.
-     * This can be used only if direction Type is Direction.PathAndRotation or Direction.Rotation.
-     * With PathAndRotation the target will face the direction of the path multiplied by the given Quaternion.
-     * With Rotation the rotation of the target will be set with the given Quaternion.
-     * @param rotation the rotation quaternion.
-     */
-    public void setRotation(Quaternion rotation) {
-        this.rotation = rotation;
-    }
-
-    /**
-     * Return the motion path this control follows.
-     * @return
-     */
-    public MotionPath getPath() {
-        return path;
-    }
-
-    /**
-     * Sets the motion path to follow.
-     * @param path
-     */
-    public void setPath(MotionPath path) {
-        this.path = path;
-    }
-
-    public void setEnabled(boolean enabled) {
-        if (enabled) {
-            play();
-        } else {
-            pause();
-        }
-    }
-
-    public boolean isEnabled() {
-        return playState != PlayState.Stopped;
-    }
-
-    public void render(RenderManager rm, ViewPort vp) {
-    }
-
-    public void setSpatial(Spatial spatial) {
-        this.spatial = spatial;
-    }
-
-    public Spatial getSpatial() {
-        return spatial;
-    }
-
-    /**
-     * Return the distance traveled by the spatial on the path.
-     * @return 
-     */
-    public float getTraveledDistance() {
-        return traveledDistance;
-    }
-}
+/*
+ * Copyright (c) 2009-2016 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.cinematic.events;
+
+import com.jme3.animation.AnimationUtils;
+import com.jme3.animation.LoopMode;
+import com.jme3.app.Application;
+import com.jme3.cinematic.Cinematic;
+import com.jme3.cinematic.MotionPath;
+import com.jme3.cinematic.PlayState;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.math.Quaternion;
+import com.jme3.math.Vector3f;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.control.Control;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+import java.io.IOException;
+
+/**
+ * A MotionEvent is a control over the spatial that manages the position and direction of the spatial while following a motion Path.
+ *
+ * You must first create a MotionPath and then create a MotionEvent to associate a spatial and the path.
+ *
+ * @author Nehon
+ */
+public class MotionEvent extends AbstractCinematicEvent implements Control, JmeCloneable {
+
+    protected Spatial spatial;
+    protected int currentWayPoint;
+    protected float currentValue;
+    protected Vector3f direction = new Vector3f();
+    protected Vector3f lookAt = null;
+    protected Vector3f upVector = Vector3f.UNIT_Y;
+    protected Quaternion rotation = null;
+    protected Direction directionType = Direction.None;
+    protected MotionPath path;
+    private boolean isControl = true;
+    private int travelDirection = 1;
+    /**
+     * the distance traveled by the spatial on the path
+     */
+    protected float traveledDistance = 0;
+
+    /**
+     * Enum for the different type of target direction behavior.
+     */
+    public enum Direction {
+
+        /**
+         * The target stays in the starting direction.
+         */
+        None,
+        /**
+         * The target rotates with the direction of the path.
+         */
+        Path,
+        /**
+         * The target rotates with the direction of the path but with the addition of a rotation.
+         * You need to use the setRotation method when using this Direction.
+         */
+        PathAndRotation,
+        /**
+         * The target rotates with the given rotation.
+         */
+        Rotation,
+        /**
+         * The target looks at a point.
+         * You need to use the setLookAt method when using this direction.
+         */
+        LookAt
+    }
+
+    /**
+     * Create MotionEvent,
+     * when using this constructor don't forget to assign spatial and path.
+     */
+    public MotionEvent() {
+        super();
+    }
+    
+    /**
+     * Creates a MotionPath for the given spatial on the given motion path.
+     * @param spatial
+     * @param path
+     */
+    public MotionEvent(Spatial spatial, MotionPath path) {
+        super();
+        spatial.addControl(this);
+        this.path = path;
+    }
+
+    /**
+     * Creates a MotionPath for the given spatial on the given motion path.
+     * @param spatial
+     * @param path
+     */
+    public MotionEvent(Spatial spatial, MotionPath path, float initialDuration) {
+        super(initialDuration);
+        spatial.addControl(this);
+        this.path = path;
+    }
+
+    /**
+     * Creates a MotionPath for the given spatial on the given motion path.
+     * @param spatial
+     * @param path
+     */
+    public MotionEvent(Spatial spatial, MotionPath path, LoopMode loopMode) {
+        super();
+        spatial.addControl(this);
+        this.path = path;
+        this.loopMode = loopMode;
+    }
+
+    /**
+     * Creates a MotionPath for the given spatial on the given motion path.
+     * @param spatial
+     * @param path
+     */
+    public MotionEvent(Spatial spatial, MotionPath path, float initialDuration, LoopMode loopMode) {
+        super(initialDuration);
+        spatial.addControl(this);
+        this.path = path;
+        this.loopMode = loopMode;
+    }
+
+    public void update(float tpf) {
+        if (isControl) {
+            internalUpdate(tpf);
+        }
+    }
+
+    @Override
+    public void internalUpdate(float tpf) {
+        if (playState == PlayState.Playing) {
+            time = time + (tpf * speed);
+            if (loopMode == LoopMode.Loop && time < 0) {
+                time = initialDuration;
+            }            
+            if ((time >= initialDuration || time < 0) && loopMode == LoopMode.DontLoop) {
+                if (time >= initialDuration) {
+                    path.triggerWayPointReach(path.getNbWayPoints() - 1, this);
+                }
+                stop();
+            } else {
+                time = AnimationUtils.clampWrapTime(time, initialDuration, loopMode);
+                if(time<0){
+                    speed = - speed;
+                    time = - time;
+                }
+                onUpdate(tpf);
+            }
+        }
+    }
+
+    @Override
+    public void initEvent(Application app, Cinematic cinematic) {
+        super.initEvent(app, cinematic);
+        isControl = false;
+    }
+
+    @Override
+    public void setTime(float time) {
+        super.setTime(time);
+        onUpdate(0);
+    }
+
+    public void onUpdate(float tpf) {
+        traveledDistance = path.interpolatePath(time, this, tpf);
+        computeTargetDirection();
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(lookAt, "lookAt", null);
+        oc.write(upVector, "upVector", Vector3f.UNIT_Y);
+        oc.write(rotation, "rotation", null);
+        oc.write(directionType, "directionType", Direction.None);
+        oc.write(path, "path", null);
+        oc.write(spatial, "spatial", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule in = im.getCapsule(this);
+        lookAt = (Vector3f) in.readSavable("lookAt", null);
+        upVector = (Vector3f) in.readSavable("upVector", Vector3f.UNIT_Y);
+        rotation = (Quaternion) in.readSavable("rotation", null);
+        directionType = in.readEnum("directionType", Direction.class, Direction.None);
+        path = (MotionPath) in.readSavable("path", null);
+        spatial = (Spatial) in.readSavable("spatial", null);
+    }
+
+    /**
+     * This method is meant to be called by the motion path only.
+     * @return
+     */
+    public boolean needsDirection() {
+        return directionType == Direction.Path || directionType == Direction.PathAndRotation;
+    }
+
+    private void computeTargetDirection() {
+        switch (directionType) {
+            case Path:
+                Quaternion q = new Quaternion();
+                q.lookAt(direction, upVector);
+                spatial.setLocalRotation(q);
+                break;
+            case LookAt:
+                if (lookAt != null) {
+                    spatial.lookAt(lookAt, upVector);
+                }
+                break;
+            case PathAndRotation:
+                if (rotation != null) {
+                    Quaternion q2 = new Quaternion();
+                    q2.lookAt(direction, upVector);
+                    q2.multLocal(rotation);
+                    spatial.setLocalRotation(q2);
+                }
+                break;
+            case Rotation:
+                if (rotation != null) {
+                    spatial.setLocalRotation(rotation);
+                }
+                break;
+            case None:
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Clone this control for the given spatial.
+     * @param spatial
+     * @return
+     */
+    @Override
+    public Control cloneForSpatial(Spatial spatial) {
+        MotionEvent control = new MotionEvent();
+        control.setPath(path);
+        control.playState = playState;
+        control.currentWayPoint = currentWayPoint;
+        control.currentValue = currentValue;
+        control.direction = direction.clone();
+        control.lookAt = lookAt;
+        control.upVector = upVector.clone();
+        control.rotation = rotation;
+        control.initialDuration = initialDuration;
+        control.speed = speed;
+        control.loopMode = loopMode;
+        control.directionType = directionType;
+
+        return control;
+    }
+
+    @Override   
+    public Object jmeClone() {
+        MotionEvent control = new MotionEvent();
+        control.path = path;
+        control.playState = playState;
+        control.currentWayPoint = currentWayPoint;
+        control.currentValue = currentValue;
+        control.direction = direction.clone();
+        control.lookAt = lookAt;
+        control.upVector = upVector.clone();
+        control.rotation = rotation;
+        control.initialDuration = initialDuration;
+        control.speed = speed;
+        control.loopMode = loopMode;
+        control.directionType = directionType;
+        control.spatial = spatial;
+
+        return control;
+    }     
+
+    @Override   
+    public void cloneFields( Cloner cloner, Object original ) { 
+        this.spatial = cloner.clone(spatial);
+    }
+         
+    @Override
+    public void onPlay() {
+        traveledDistance = 0;
+    }
+
+    @Override
+    public void onStop() {
+        currentWayPoint = 0;
+    }
+
+    @Override
+    public void onPause() {
+    }
+
+    /**
+     * This method is meant to be called by the motion path only.
+     * @return
+     */
+    public float getCurrentValue() {
+        return currentValue;
+    }
+
+    /**
+     * This method is meant to be called by the motion path only.
+     *
+     */
+    public void setCurrentValue(float currentValue) {
+        this.currentValue = currentValue;
+    }
+
+    /**
+     * This method is meant to be called by the motion path only.
+     * @return
+     */
+    public int getCurrentWayPoint() {
+        return currentWayPoint;
+    }
+
+    /**
+     * This method is meant to be called by the motion path only.
+     *
+     */
+    public void setCurrentWayPoint(int currentWayPoint) {
+        this.currentWayPoint = currentWayPoint;
+    }
+
+    /**
+     * Returns the direction the spatial is moving.
+     * @return
+     */
+    public Vector3f getDirection() {
+        return direction;
+    }
+
+    /**
+     * Sets the direction of the spatial, using the Y axis as the up vector.
+     * Use MotionEvent#setDirection((Vector3f direction,Vector3f upVector) if 
+     * you want a custum up vector.
+     * This method is used by the motion path.
+     * @param direction
+     */
+    public void setDirection(Vector3f direction) {
+        setDirection(direction, Vector3f.UNIT_Y); 
+   }
+    
+    /**
+     * Sets the direction of the spatial with the given up vector.
+     * This method is used by the motion path.
+     * @param direction
+     * @param upVector the up vector to consider for this direction.
+     */
+    public void setDirection(Vector3f direction,Vector3f upVector) {
+        this.direction.set(direction);
+        this.upVector.set(upVector);
+    }
+
+    /**
+     * Returns the direction type of the target.
+     * @return the direction type.
+     */
+    public Direction getDirectionType() {
+        return directionType;
+    }
+
+    /**
+     * Sets the direction type of the target.
+     * On each update the direction given to the target can have different behavior.
+     * See the Direction Enum for explanations.
+     * @param directionType the direction type.
+     */
+    public void setDirectionType(Direction directionType) {
+        this.directionType = directionType;
+    }
+
+    /**
+     * Set the lookAt for the target.
+     * This can be used only if direction Type is Direction.LookAt.
+     * @param lookAt the position to look at.
+     * @param upVector the up vector.
+     */
+    public void setLookAt(Vector3f lookAt, Vector3f upVector) {
+        this.lookAt = lookAt;
+        this.upVector = upVector;
+    }
+
+    /**
+     * Returns the rotation of the target.
+     * @return the rotation quaternion.
+     */
+    public Quaternion getRotation() {
+        return rotation;
+    }
+
+    /**
+     * Sets the rotation of the target.
+     * This can be used only if direction Type is Direction.PathAndRotation or Direction.Rotation.
+     * With PathAndRotation the target will face the direction of the path multiplied by the given Quaternion.
+     * With Rotation the rotation of the target will be set with the given Quaternion.
+     * @param rotation the rotation quaternion.
+     */
+    public void setRotation(Quaternion rotation) {
+        this.rotation = rotation;
+    }
+
+    /**
+     * Return the motion path this control follows.
+     * @return
+     */
+    public MotionPath getPath() {
+        return path;
+    }
+
+    /**
+     * Sets the motion path to follow.
+     * @param path
+     */
+    public void setPath(MotionPath path) {
+        this.path = path;
+    }
+
+    public void setEnabled(boolean enabled) {
+        if (enabled) {
+            play();
+        } else {
+            pause();
+        }
+    }
+
+    public boolean isEnabled() {
+        return playState != PlayState.Stopped;
+    }
+
+    public void render(RenderManager rm, ViewPort vp) {
+    }
+
+    public void setSpatial(Spatial spatial) {
+        this.spatial = spatial;
+    }
+
+    public Spatial getSpatial() {
+        return spatial;
+    }
+
+    /**
+     * Return the distance traveled by the spatial on the path.
+     * @return 
+     */
+    public float getTraveledDistance() {
+        return traveledDistance;
+    }
+}

+ 230 - 229
jme3-core/src/main/java/com/jme3/cinematic/events/SoundEvent.java

@@ -1,229 +1,230 @@
-/*
- * Copyright (c) 2009-2012 jMonkeyEngine
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * * Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- *
- * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
- *   may be used to endorse or promote products derived from this software
- *   without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.jme3.cinematic.events;
-
-import com.jme3.animation.LoopMode;
-import com.jme3.app.Application;
-import com.jme3.audio.AudioNode;
-import com.jme3.audio.AudioSource;
-import com.jme3.cinematic.Cinematic;
-import com.jme3.export.InputCapsule;
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.export.OutputCapsule;
-import java.io.IOException;
-
-/**
- * A sound track to be played in a cinematic.
- * @author Nehon
- */
-public class SoundEvent extends AbstractCinematicEvent {
-
-    protected String path;
-    protected AudioNode audioNode;
-    protected boolean stream = false;
-
-    /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
-     */
-    public SoundEvent(String path) {
-        this.path = path;
-    }
-
-    /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
-     * @param stream true to make the audio data streamed
-     */
-    public SoundEvent(String path, boolean stream) {
-        this(path);
-        this.stream = stream;
-    }
-
-    /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
-     * @param stream true to make the audio data streamed
-     * @param initialDuration the initial duration of the event
-     */
-    public SoundEvent(String path, boolean stream, float initialDuration) {
-        super(initialDuration);
-        this.path = path;
-        this.stream = stream;
-    }
-
-    /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
-     * @param stream true to make the audio data streamed
-     * @param loopMode the loopMode 
-     * @see LoopMode
-     */
-    public SoundEvent(String path, boolean stream, LoopMode loopMode) {
-        super(loopMode);
-        this.path = path;
-        this.stream = stream;
-    }
-
-     /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
-     * @param stream true to make the audio data streamed
-     * @param initialDuration the initial duration of the event
-     * @param loopMode the loopMode 
-     * @see LoopMode
-     */
-    public SoundEvent(String path, boolean stream, float initialDuration, LoopMode loopMode) {
-        super(initialDuration, loopMode);
-        this.path = path;
-        this.stream = stream;
-    }
-
-     /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")    
-     * @param initialDuration the initial duration of the event
-     */
-    public SoundEvent(String path, float initialDuration) {
-        super(initialDuration);
-        this.path = path;
-    }
-
-     /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")   
-     * @param loopMode the loopMode 
-     * @see LoopMode
-     */
-    public SoundEvent(String path, LoopMode loopMode) {
-        super(loopMode);
-        this.path = path;
-    }
-
-     /**
-     * creates a sound track from the given resource path
-     * @param path the path to an audio file (ie : "Sounds/mySound.wav")    
-     * @param initialDuration the initial duration of the event
-     * @param loopMode the loopMode 
-     * @see LoopMode
-     */
-    public SoundEvent(String path, float initialDuration, LoopMode loopMode) {
-        super(initialDuration, loopMode);
-        this.path = path;
-    }
-
-    /**
-     * creates a sound event
-     * used for serialization
-     */
-    public SoundEvent() {
-    }
-
-    @Override
-    public void initEvent(Application app, Cinematic cinematic) {
-        super.initEvent(app, cinematic);
-        audioNode = new AudioNode(app.getAssetManager(), path, stream);
-        audioNode.setPositional(false);
-        setLoopMode(loopMode);
-    }
-
-    @Override
-    public void setTime(float time) {
-        super.setTime(time);
-        //can occur on rewind
-        if (time < 0f) {            
-            stop();
-        }else{
-            audioNode.setTimeOffset(time);
-        }
-    }
-
-    @Override
-    public void onPlay() {
-        audioNode.play();
-    }
-
-    @Override
-    public void onStop() {
-        audioNode.stop();
-
-    }
-
-    @Override
-    public void onPause() {
-        audioNode.pause();
-    }
-
-    @Override
-    public void onUpdate(float tpf) {
-        if (audioNode.getStatus() == AudioSource.Status.Stopped) {
-            stop();
-        }
-    }
-
-    /**
-     *  Returns the underlying audio node of this sound track
-     * @return
-     */
-    public AudioNode getAudioNode() {
-        return audioNode;
-    }
-
-    @Override
-    public void setLoopMode(LoopMode loopMode) {
-        super.setLoopMode(loopMode);
-
-        if (loopMode != LoopMode.DontLoop) {
-            audioNode.setLooping(true);
-        } else {
-            audioNode.setLooping(false);
-        }
-    }
-
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        OutputCapsule oc = ex.getCapsule(this);
-        oc.write(path, "path", "");
-        oc.write(stream, "stream", false);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        InputCapsule ic = im.getCapsule(this);
-        path = ic.readString("path", "");
-        stream = ic.readBoolean("stream", false);
-
-    }
-}
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.cinematic.events;
+
+import com.jme3.animation.LoopMode;
+import com.jme3.app.Application;
+import com.jme3.audio.AudioNode;
+import com.jme3.audio.AudioSource;
+import com.jme3.cinematic.Cinematic;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import java.io.IOException;
+
+/**
+ * A sound track to be played in a cinematic.
+ * @author Nehon
+ */
+public class SoundEvent extends AbstractCinematicEvent {
+
+    protected String path;
+    protected AudioNode audioNode;
+    protected boolean stream = false;
+
+    /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
+     */
+    public SoundEvent(String path) {
+        this.path = path;
+    }
+
+    /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
+     * @param stream true to make the audio data streamed
+     */
+    public SoundEvent(String path, boolean stream) {
+        this(path);
+        this.stream = stream;
+    }
+
+    /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
+     * @param stream true to make the audio data streamed
+     * @param initialDuration the initial duration of the event
+     */
+    public SoundEvent(String path, boolean stream, float initialDuration) {
+        super(initialDuration);
+        this.path = path;
+        this.stream = stream;
+    }
+
+    /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
+     * @param stream true to make the audio data streamed
+     * @param loopMode the loopMode 
+     * @see LoopMode
+     */
+    public SoundEvent(String path, boolean stream, LoopMode loopMode) {
+        super(loopMode);
+        this.path = path;
+        this.stream = stream;
+    }
+
+     /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")
+     * @param stream true to make the audio data streamed
+     * @param initialDuration the initial duration of the event
+     * @param loopMode the loopMode 
+     * @see LoopMode
+     */
+    public SoundEvent(String path, boolean stream, float initialDuration, LoopMode loopMode) {
+        super(initialDuration, loopMode);
+        this.path = path;
+        this.stream = stream;
+    }
+
+     /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")    
+     * @param initialDuration the initial duration of the event
+     */
+    public SoundEvent(String path, float initialDuration) {
+        super(initialDuration);
+        this.path = path;
+    }
+
+     /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")   
+     * @param loopMode the loopMode 
+     * @see LoopMode
+     */
+    public SoundEvent(String path, LoopMode loopMode) {
+        super(loopMode);
+        this.path = path;
+    }
+
+     /**
+     * creates a sound track from the given resource path
+     * @param path the path to an audio file (ie : "Sounds/mySound.wav")    
+     * @param initialDuration the initial duration of the event
+     * @param loopMode the loopMode 
+     * @see LoopMode
+     */
+    public SoundEvent(String path, float initialDuration, LoopMode loopMode) {
+        super(initialDuration, loopMode);
+        this.path = path;
+    }
+
+    /**
+     * creates a sound event
+     * used for serialization
+     */
+    public SoundEvent() {
+        super();
+    }
+
+    @Override
+    public void initEvent(Application app, Cinematic cinematic) {
+        super.initEvent(app, cinematic);
+        audioNode = new AudioNode(app.getAssetManager(), path, stream);
+        audioNode.setPositional(false);
+        setLoopMode(loopMode);
+    }
+
+    @Override
+    public void setTime(float time) {
+        super.setTime(time);
+        //can occur on rewind
+        if (time < 0f) {            
+            stop();
+        }else{
+            audioNode.setTimeOffset(time);
+        }
+    }
+
+    @Override
+    public void onPlay() {
+        audioNode.play();
+    }
+
+    @Override
+    public void onStop() {
+        audioNode.stop();
+
+    }
+
+    @Override
+    public void onPause() {
+        audioNode.pause();
+    }
+
+    @Override
+    public void onUpdate(float tpf) {
+        if (audioNode.getStatus() == AudioSource.Status.Stopped) {
+            stop();
+        }
+    }
+
+    /**
+     *  Returns the underlying audio node of this sound track
+     * @return
+     */
+    public AudioNode getAudioNode() {
+        return audioNode;
+    }
+
+    @Override
+    public void setLoopMode(LoopMode loopMode) {
+        super.setLoopMode(loopMode);
+
+        if (loopMode != LoopMode.DontLoop) {
+            audioNode.setLooping(true);
+        } else {
+            audioNode.setLooping(false);
+        }
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(path, "path", "");
+        oc.write(stream, "stream", false);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        path = ic.readString("path", "");
+        stream = ic.readBoolean("stream", false);
+
+    }
+}

+ 123 - 122
jme3-core/src/main/java/com/jme3/scene/CameraNode.java

@@ -1,122 +1,123 @@
-/*
- * Copyright (c) 2009-2012 jMonkeyEngine
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- * * Redistributions of source code must retain the above copyright
- *   notice, this list of conditions and the following disclaimer.
- *
- * * Redistributions in binary form must reproduce the above copyright
- *   notice, this list of conditions and the following disclaimer in the
- *   documentation and/or other materials provided with the distribution.
- *
- * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
- *   may be used to endorse or promote products derived from this software
- *   without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
- * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
- * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.jme3.scene;
-
-import com.jme3.export.JmeExporter;
-import com.jme3.export.JmeImporter;
-import com.jme3.renderer.Camera;
-import com.jme3.scene.control.CameraControl;
-import com.jme3.scene.control.CameraControl.ControlDirection;
-import com.jme3.util.clone.Cloner;
-import java.io.IOException;
-
-/**
- * <code>CameraNode</code> simply uses {@link CameraControl} to implement
- * linking of camera and node data.
- *
- * @author Tim8Dev
- */
-public class CameraNode extends Node {
-
-    private CameraControl camControl;
-
-    /**
-     * Serialization only. Do not use.
-     */
-    public CameraNode() {
-    }
-
-    public CameraNode(String name, Camera camera) {
-        this(name, new CameraControl(camera));
-    }
-
-    public CameraNode(String name, CameraControl control) {
-        super(name);
-        addControl(control);
-        camControl = control;
-    }
-
-    public void setEnabled(boolean enabled) {
-        camControl.setEnabled(enabled);
-    }
-
-    public boolean isEnabled() {
-        return camControl.isEnabled();
-    }
-
-    public void setControlDir(ControlDirection controlDir) {
-        camControl.setControlDir(controlDir);
-    }
-
-    public void setCamera(Camera camera) {
-        camControl.setCamera(camera);
-    }
-
-    public ControlDirection getControlDir() {
-        return camControl.getControlDir();
-    }
-
-    public Camera getCamera() {
-        return camControl.getCamera();
-    }
-
-//    @Override
-//    public void lookAt(Vector3f position, Vector3f upVector) {
-//        this.lookAt(position, upVector);
-//        camControl.getCamera().lookAt(position, upVector);
-//    }
-
-    /**
-     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
-     */
-    @Override
-    public void cloneFields( Cloner cloner, Object original ) {
-        super.cloneFields(cloner, original);
-
-        // A change in behavior... I think previously CameraNode was probably
-        // not really cloneable... or at least its camControl would be pointing
-        // to the wrong control. -pspeed
-        this.camControl = cloner.clone(camControl);
-    }
-
-    @Override
-    public void read(JmeImporter im) throws IOException {
-        super.read(im);
-        camControl = (CameraControl)im.getCapsule(this).readSavable("camControl", null);
-    }
-
-    @Override
-    public void write(JmeExporter ex) throws IOException {
-        super.write(ex);
-        ex.getCapsule(this).write(camControl, "camControl", null);
-    }
-}
+/*
+ * Copyright (c) 2009-2012 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.scene;
+
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.renderer.Camera;
+import com.jme3.scene.control.CameraControl;
+import com.jme3.scene.control.CameraControl.ControlDirection;
+import com.jme3.util.clone.Cloner;
+import java.io.IOException;
+
+/**
+ * <code>CameraNode</code> simply uses {@link CameraControl} to implement
+ * linking of camera and node data.
+ *
+ * @author Tim8Dev
+ */
+public class CameraNode extends Node {
+
+    private CameraControl camControl;
+
+    /**
+     * Serialization only. Do not use.
+     */
+    public CameraNode() {
+        super();
+    }
+
+    public CameraNode(String name, Camera camera) {
+        this(name, new CameraControl(camera));
+    }
+
+    public CameraNode(String name, CameraControl control) {
+        super(name);
+        addControl(control);
+        camControl = control;
+    }
+
+    public void setEnabled(boolean enabled) {
+        camControl.setEnabled(enabled);
+    }
+
+    public boolean isEnabled() {
+        return camControl.isEnabled();
+    }
+
+    public void setControlDir(ControlDirection controlDir) {
+        camControl.setControlDir(controlDir);
+    }
+
+    public void setCamera(Camera camera) {
+        camControl.setCamera(camera);
+    }
+
+    public ControlDirection getControlDir() {
+        return camControl.getControlDir();
+    }
+
+    public Camera getCamera() {
+        return camControl.getCamera();
+    }
+
+//    @Override
+//    public void lookAt(Vector3f position, Vector3f upVector) {
+//        this.lookAt(position, upVector);
+//        camControl.getCamera().lookAt(position, upVector);
+//    }
+
+    /**
+     *  Called internally by com.jme3.util.clone.Cloner.  Do not call directly.
+     */
+    @Override
+    public void cloneFields( Cloner cloner, Object original ) {
+        super.cloneFields(cloner, original);
+
+        // A change in behavior... I think previously CameraNode was probably
+        // not really cloneable... or at least its camControl would be pointing
+        // to the wrong control. -pspeed
+        this.camControl = cloner.clone(camControl);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        camControl = (CameraControl)im.getCapsule(this).readSavable("camControl", null);
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        ex.getCapsule(this).write(camControl, "camControl", null);
+    }
+}