diff --git a/engine/src/core/com/jme3/animation/AnimControl.java b/engine/src/core/com/jme3/animation/AnimControl.java index 590801c94..000b40b76 100644 --- a/engine/src/core/com/jme3/animation/AnimControl.java +++ b/engine/src/core/com/jme3/animation/AnimControl.java @@ -43,6 +43,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.Map.Entry; /** * AnimControl is a Spatial control that allows manipulation @@ -118,9 +119,12 @@ public final class AnimControl extends AbstractControl implements Cloneable { clone.skeleton = new Skeleton(skeleton); } - // animationMap is reference-copied, animation data should be shared - // to reduce memory usage. - + // animationMap is cloned, but only ClonableTracks will be cloned as they need a reference to a cloned spatial + clone.animationMap = new HashMap(); + for (Entry animEntry : animationMap.entrySet()) { + clone.animationMap.put(animEntry.getKey(), animEntry.getValue().cloneForSpatial(spatial)); + } + return clone; } catch (CloneNotSupportedException ex) { throw new AssertionError(); diff --git a/engine/src/core/com/jme3/animation/Animation.java b/engine/src/core/com/jme3/animation/Animation.java index 2c461a6d2..5ea707b63 100644 --- a/engine/src/core/com/jme3/animation/Animation.java +++ b/engine/src/core/com/jme3/animation/Animation.java @@ -32,6 +32,7 @@ package com.jme3.animation; import com.jme3.export.*; +import com.jme3.scene.Spatial; import com.jme3.util.SafeArrayList; import com.jme3.util.TempVars; import java.io.IOException; @@ -42,27 +43,26 @@ import java.io.IOException; * @author Kirill Vainer, Marcin Roguski (Kaelthas) */ public class Animation implements Savable, Cloneable { - + /** * The name of the animation. */ private String name; - /** * The length of the animation. */ private float length; - /** * The tracks of the animation. */ private SafeArrayList tracks = new SafeArrayList(Track.class); - + /** * Serialization-only. Do not use. */ - public Animation() {} - + public Animation() { + } + /** * Creates a new Animation with the given name and length. * @@ -73,24 +73,24 @@ public class Animation implements Savable, Cloneable { this.name = name; this.length = length; } - + /** * The name of the bone animation * @return name of the bone animation */ public String getName() { - return name; + return name; } - + /** * Returns the length in seconds of this animation * * @return the length in seconds of this animation */ public float getLength() { - return length; + return length; } - + /** * This method sets the current time of the animation. * This method behaves differently for every known track type. @@ -102,58 +102,61 @@ public class Animation implements Savable, Cloneable { * @param channel the animation channel */ void setTime(float time, float blendAmount, AnimControl control, AnimChannel channel, TempVars vars) { - if (tracks == null) + if (tracks == null) { return; - + } + for (Track track : tracks) { track.setTime(time, blendAmount, control, channel, vars); } } - + /** * Set the {@link Track}s to be used by this animation. *

* * @param tracks The tracks to set. */ - public void setTracks(Track[] tracksArray){ + public void setTracks(Track[] tracksArray) { for (Track track : tracksArray) { tracks.add(track); } } - - + /** * Adds a track to this animation * @param track the track to add */ - public void addTrack(Track track){ + public void addTrack(Track track) { tracks.add(track); } - + /** * removes a track from this animation * @param track the track to remove - */ - public void removeTrack(Track track){ + */ + public void removeTrack(Track track) { tracks.remove(track); - } - + if (track instanceof ClonableTrack) { + ((ClonableTrack) track).cleanUp(); + } + } + /** * Returns the tracks set in {@link #setTracks(com.jme3.animation.Track[]) }. * * @return the tracks set previously */ public Track[] getTracks() { - return tracks.getArray(); + return tracks.getArray(); } - + /** * This method creates a clone of the current object. * @return a clone of the current object */ - @Override - public Animation clone() { + @Override + public Animation clone() { try { Animation result = (Animation) super.clone(); result.tracks = new SafeArrayList(Track.class); @@ -166,12 +169,34 @@ public class Animation implements Savable, Cloneable { } } + /** + * + * @param spat + * @return + */ + public Animation cloneForSpatial(Spatial spat) { + try { + Animation result = (Animation) super.clone(); + result.tracks = new SafeArrayList(Track.class); + for (Track track : tracks) { + if (track instanceof ClonableTrack) { + result.tracks.add(((ClonableTrack) track).cloneForSpatial(spat)); + } else { + result.tracks.add(track); + } + } + return result; + } catch (CloneNotSupportedException e) { + throw new AssertionError(); + } + } + @Override public String toString() { return getClass().getSimpleName() + "[name=" + name + ", length=" + length + ']'; } - - @Override + + @Override public void write(JmeExporter ex) throws IOException { OutputCapsule out = ex.getCapsule(this); out.write(name, "name", null); @@ -184,7 +209,7 @@ public class Animation implements Savable, Cloneable { InputCapsule in = im.getCapsule(this); name = in.readString("name", null); length = in.readFloat("length", 0f); - + Savable[] arr = in.readSavableArray("tracks", null); if (arr != null) { // NOTE: Backward compat only .. Some animations have no @@ -193,8 +218,8 @@ public class Animation implements Savable, Cloneable { // its only appropriate that the check is made here as well. tracks = new SafeArrayList(Track.class); for (Savable savable : arr) { - tracks.add((Track)savable); - } + tracks.add((Track) savable); + } } } } diff --git a/engine/src/core/com/jme3/animation/AudioTrack.java b/engine/src/core/com/jme3/animation/AudioTrack.java index 5d59ac9e2..68ef3c879 100644 --- a/engine/src/core/com/jme3/animation/AudioTrack.java +++ b/engine/src/core/com/jme3/animation/AudioTrack.java @@ -36,8 +36,12 @@ import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; import com.jme3.util.TempVars; import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; /** * AudioTrack is a track to add to an existing animation, to paly a sound during an animations @@ -55,8 +59,9 @@ import java.io.IOException; * * @author Nehon */ -public class AudioTrack implements Track { +public class AudioTrack implements ClonableTrack { + private static final Logger logger = Logger.getLogger(AudioTrack.class.getName()); private AudioNode audio; private float startOffset = 0; private float length = 0; @@ -89,6 +94,7 @@ public class AudioTrack implements Track { public AudioTrack(AudioNode audio, float length) { this.audio = audio; this.length = length; + setUserData(this); } /** @@ -141,6 +147,80 @@ public class AudioTrack implements Track { return new AudioTrack(audio, length, startOffset); } + /** + * This method clone the Track and search for the cloned counterpart of the original audio node in the given cloned spatial. + * The spatial is assumed to be the Spatial holding the AnimControl controling the animation using this Track. + * @param spatial the Spatial holding the AnimControl + * @return the cloned Track with proper reference + */ + public Track cloneForSpatial(Spatial spatial) { + AudioTrack audioTrack = new AudioTrack(); + audioTrack.length = this.length; + audioTrack.startOffset = this.startOffset; + + //searching for the newly cloned AudioNode + audioTrack.audio = findAudio(spatial); + if (audioTrack.audio == null) { + logger.log(Level.WARNING, "{0} was not found in {1} or is not bound to this track", new Object[]{audio.getName(), spatial.getName()}); + audioTrack.audio = audio; + } + + //setting user data on the new AudioNode and marking it with a reference to the cloned Track. + setUserData(audioTrack); + + return audioTrack; + } + + /** + * recursive function responsible for finding the newly cloned AudioNode + * @param spat + * @return + */ + private AudioNode findAudio(Spatial spat) { + if (spat instanceof AudioNode) { + //spat is an AudioNode + AudioNode em = (AudioNode) spat; + //getting the UserData TrackInfo so check if it should be attached to this Track + TrackInfo t = (TrackInfo) em.getUserData("TrackInfo"); + if (t != null && t.getTracks().contains(this)) { + return em; + } + return null; + + } else if (spat instanceof Node) { + for (Spatial child : ((Node) spat).getChildren()) { + AudioNode em = findAudio(child); + if (em != null) { + return em; + } + } + } + return null; + } + + private void setUserData(AudioTrack audioTrack) { + //fetching the UserData TrackInfo. + TrackInfo data = (TrackInfo) audioTrack.audio.getUserData("TrackInfo"); + + //if it does not exist, we create it and attach it to the AudioNode. + if (data == null) { + data = new TrackInfo(); + audioTrack.audio.setUserData("TrackInfo", data); + } + + //adding the given Track to the TrackInfo. + data.addTrack(audioTrack); + } + + public void cleanUp() { + TrackInfo t = (TrackInfo) audio.getUserData("TrackInfo"); + t.getTracks().remove(this); + if(!t.getTracks().isEmpty()){ + audio.setUserData("TrackInfo", null); + } + } + + /** * * @return the audio node used by this track @@ -154,7 +234,12 @@ public class AudioTrack implements Track { * @param audio */ public void setAudio(AudioNode audio) { + if (this.audio != null) { + TrackInfo data = (TrackInfo) audio.getUserData("TrackInfo"); + data.getTracks().remove(this); + } this.audio = audio; + setUserData(this); } /** diff --git a/engine/src/core/com/jme3/animation/ClonableTrack.java b/engine/src/core/com/jme3/animation/ClonableTrack.java new file mode 100644 index 000000000..033042798 --- /dev/null +++ b/engine/src/core/com/jme3/animation/ClonableTrack.java @@ -0,0 +1,40 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.jme3.animation; + +import com.jme3.scene.Spatial; + +/** + * An interface that allow to clone a Track for a given Spatial. + * The spatial fed to the method is the Spatial holding the AnimControl controling the Animation using this track. + * + * Implement this interface only if you make your own Savable Track and that the track has a direct reference to a Spatial in the scene graph. + * This Spatial is assumed to be a child of the spatial holding the AnimControl. + * + * + * @author Nehon + */ +public interface ClonableTrack extends Track { + + /** + * Allows to clone the track for a given Spatial. + * The spatial fed to the method is the Spatial holding the AnimControl controling the Animation using this track. + * This method will be called during the loading process of a j3o model by the assetManager. + * The assetManager keeps the original model in cache and returns a clone of the model. + * + * This method prupose is to find the cloned reference of the original spatial which it refers to in the cloned model. + * + * See EffectTrack for a proper implementation. + * + * @param spatial the spatial holding the AnimControl + * @return the cloned Track + */ + public Track cloneForSpatial(Spatial spatial); + + /** + * Method responsible of cleaning UserData on referenced Spatials when the Track is deleted + */ + public void cleanUp(); +} diff --git a/engine/src/core/com/jme3/animation/EffectTrack.java b/engine/src/core/com/jme3/animation/EffectTrack.java index 1773e9260..6085ad798 100644 --- a/engine/src/core/com/jme3/animation/EffectTrack.java +++ b/engine/src/core/com/jme3/animation/EffectTrack.java @@ -38,12 +38,15 @@ import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; +import com.jme3.scene.Node; import com.jme3.scene.Spatial; import com.jme3.scene.Spatial.CullHint; import com.jme3.scene.control.AbstractControl; import com.jme3.scene.control.Control; import com.jme3.util.TempVars; import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; /** * EffectTrack is a track to add to an existing animation, to emmit particles during animations @@ -62,8 +65,9 @@ import java.io.IOException; * * @author Nehon */ -public class EffectTrack implements Track { - +public class EffectTrack implements ClonableTrack { + + private static final Logger logger = Logger.getLogger(EffectTrack.class.getName()); private ParticleEmitter emitter; private float startOffset = 0; private float particlesPerSeconds = 0; @@ -71,10 +75,9 @@ public class EffectTrack implements Track { private boolean emitted = false; private boolean initialized = false; private boolean stopRequested = false; - //control responsible for disable and cull the emitter once all particles are gone private AbstractControl killParticles = new AbstractControl() { - + @Override protected void controlUpdate(float tpf) { if (emitter.getNumVisibleParticles() == 0) { @@ -84,29 +87,26 @@ public class EffectTrack implements Track { stopRequested = false; } } - + @Override protected void controlRender(RenderManager rm, ViewPort vp) { } - + public Control cloneForSpatial(Spatial spatial) { return null; } }; + //Anim listener that stops the Emmitter when the animation is finished or changed. private class OnEndListener implements AnimEventListener { - + public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { - if(!stopRequested){ - stop(); - } + stop(); } - + public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { - if(!stopRequested){ - stop(); - } + stop(); } } @@ -128,7 +128,9 @@ public class EffectTrack implements Track { //setting the emmitter to not emmit. this.emitter.setParticlesPerSec(0); this.length = length; - + //Marking the emitter with a reference to this track for further use in deserialization. + setUserData(this); + } /** @@ -156,14 +158,15 @@ public class EffectTrack implements Track { //checking fo time to trigger the effect if (!emitted && time >= startOffset) { emitted = true; - stopRequested = false; emitter.setCullHint(CullHint.Dynamic); emitter.setEnabled(true); //if the emitter has 0 particles per seconds emmit all particles in one shot if (particlesPerSeconds == 0) { emitter.emitAllParticles(); - emitter.addControl(killParticles); - stopRequested = true; + if (!stopRequested) { + emitter.addControl(killParticles); + stopRequested = true; + } } else { //else reset its former particlePerSec value to let it emmit. emitter.setParticlesPerSec(particlesPerSeconds); @@ -175,10 +178,15 @@ public class EffectTrack implements Track { private void stop() { emitter.setParticlesPerSec(0); emitted = false; - emitter.addControl(killParticles); - stopRequested = true; + if (!stopRequested) { + emitter.addControl(killParticles); + stopRequested = true; + } + } + + /** * Retruns the length of the track * @return length of the track @@ -194,7 +202,67 @@ public class EffectTrack implements Track { @Override public Track clone() { return new EffectTrack(emitter, length, startOffset); + } + + /** + * This method clone the Track and search for the cloned counterpart of the original emmitter in the given cloned spatial. + * The spatial is assumed to be the Spatial holding the AnimControl controling the animation using this Track. + * @param spatial the Spatial holding the AnimControl + * @return the cloned Track with proper reference + */ + public Track cloneForSpatial(Spatial spatial) { + EffectTrack effectTrack = new EffectTrack(); + effectTrack.particlesPerSeconds = this.particlesPerSeconds; + effectTrack.length = this.length; + effectTrack.startOffset = this.startOffset; + //searching for the newly cloned ParticleEmitter + effectTrack.emitter = findEmitter(spatial); + if (effectTrack.emitter == null) { + logger.log(Level.WARNING, "{0} was not found in {1} or is not bound to this track", new Object[]{emitter.getName(), spatial.getName()}); + effectTrack.emitter = emitter; + } + + //setting user data on the new emmitter and marking it with a reference to the cloned Track. + setUserData(effectTrack); + effectTrack.emitter.setParticlesPerSec(0); + return effectTrack; + } + + /** + * recursive function responsible for finding the newly cloned Emitter + * @param spat + * @return + */ + private ParticleEmitter findEmitter(Spatial spat) { + if (spat instanceof ParticleEmitter) { + //spat is a PArticleEmitter + ParticleEmitter em = (ParticleEmitter) spat; + //getting the UserData TrackInfo so check if it should be attached to this Track + TrackInfo t = (TrackInfo) em.getUserData("TrackInfo"); + if (t != null && t.getTracks().contains(this)) { + return em; + } + return null; + + } else if (spat instanceof Node) { + for (Spatial child : ((Node) spat).getChildren()) { + ParticleEmitter em = findEmitter(child); + if (em != null) { + return em; + } + } + } + return null; + } + + + public void cleanUp() { + TrackInfo t = (TrackInfo) emitter.getUserData("TrackInfo"); + t.getTracks().remove(this); + if(!t.getTracks().isEmpty()){ + emitter.setUserData("TrackInfo", null); + } } /** @@ -210,7 +278,12 @@ public class EffectTrack implements Track { * @param emitter */ public void setEmitter(ParticleEmitter emitter) { + if (this.emitter != null) { + TrackInfo data = (TrackInfo) emitter.getUserData("TrackInfo"); + data.getTracks().remove(this); + } this.emitter = emitter; + setUserData(this); } /** @@ -228,6 +301,22 @@ public class EffectTrack implements Track { public void setStartOffset(float startOffset) { this.startOffset = startOffset; } + + private void setUserData(EffectTrack effectTrack) { + //fetching the UserData TrackInfo. + TrackInfo data = (TrackInfo) effectTrack.emitter.getUserData("TrackInfo"); + + //if it does not exist, we create it and attach it to the emitter. + if (data == null) { + data = new TrackInfo(); + effectTrack.emitter.setUserData("TrackInfo", data); + } + + //adding the given Track to the TrackInfo. + data.addTrack(effectTrack); + + + } /** * Internal use only serialization @@ -236,10 +325,16 @@ public class EffectTrack implements Track { */ public void write(JmeExporter ex) throws IOException { OutputCapsule out = ex.getCapsule(this); + //reseting the particle emission rate on the emitter before saving. + emitter.setParticlesPerSec(particlesPerSeconds); + //removing eventual unpersisted control off the emitter + emitter.removeControl(killParticles); out.write(emitter, "emitter", null); + out.write(particlesPerSeconds, "particlesPerSeconds", 0); out.write(length, "length", 0); - out.write(startOffset, "startOffset", 0); + //Setting emission rate to 0 so that this track can go on being used. + emitter.setParticlesPerSec(0); } /** @@ -249,8 +344,10 @@ public class EffectTrack implements Track { */ public void read(JmeImporter im) throws IOException { InputCapsule in = im.getCapsule(this); + this.particlesPerSeconds = in.readFloat("particlesPerSeconds", 0); + //reading the emitter even if the track will then reference its cloned counter part if it's loaded with the assetManager. + //This also avoid null pointer exception if the model is not loaded via the AssetManager. emitter = (ParticleEmitter) in.readSavable("emitter", null); - this.particlesPerSeconds = emitter.getParticlesPerSec(); emitter.setParticlesPerSec(0); length = in.readFloat("length", length); startOffset = in.readFloat("startOffset", 0); diff --git a/engine/src/core/com/jme3/animation/TrackInfo.java b/engine/src/core/com/jme3/animation/TrackInfo.java new file mode 100644 index 000000000..9e82934dd --- /dev/null +++ b/engine/src/core/com/jme3/animation/TrackInfo.java @@ -0,0 +1,48 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package com.jme3.animation; + +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 java.io.IOException; +import java.util.ArrayList; + +/** + * This class is intended as a UserData added to a Spatial that is referenced by a Track. + * (ParticleEmitter for EffectTrack and AudioNode for AudioTrack) + * It holds the list of tracks that are directly referencing the Spatial. + * + * This is used when loading a Track to find the cloned reference of a Spatial in the cloned model returned by the assetManager. + * + * @author Nehon + */ +public class TrackInfo implements Savable { + + ArrayList tracks = new ArrayList(); + + public TrackInfo() { + } + + public void write(JmeExporter ex) throws IOException { + OutputCapsule c = ex.getCapsule(this); + c.writeSavableArrayList(tracks, "tracks", null); + } + + public void read(JmeImporter im) throws IOException { + InputCapsule c = im.getCapsule(this); + tracks = c.readSavableArrayList("tracks", null); + } + + public ArrayList getTracks() { + return tracks; + } + + public void addTrack(Track track) { + tracks.add(track); + } +}