diff --git a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java index daca01b843..b338ae3c6d 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2021 jMonkeyEngine + * Copyright (c) 2009-2026 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -31,11 +31,14 @@ */ package com.jme3.cinematic; +import com.jme3.anim.AnimComposer; 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.asset.AssetManager; import com.jme3.cinematic.events.AbstractCinematicEvent; +import com.jme3.cinematic.events.AnimEvent; import com.jme3.cinematic.events.CameraEvent; import com.jme3.cinematic.events.CinematicEvent; import com.jme3.export.*; @@ -43,8 +46,10 @@ import com.jme3.renderer.RenderManager; 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 java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -87,9 +92,13 @@ * * @author Nehon */ -public class Cinematic extends AbstractCinematicEvent implements AppState { +public class Cinematic extends AbstractCinematicEvent implements AppState, CinematicHandler { private static final Logger logger = Logger.getLogger(Cinematic.class.getName()); + private static final String CINEMATIC_REF_PREFIX = "Cinematic:Refs:"; + private final String CINEMATIC_REF_VALUE = "jme3:Cinematic"; + + private Application app; private Node scene; protected TimeLine timeLine = new TimeLine(); private int lastFetchedKeyFrame = -1; @@ -109,14 +118,30 @@ protected Cinematic() { super(); } + /** + * Creates a cinematic with a specific duration. + * + * @param initialDuration The total duration of the cinematic in seconds. + */ public Cinematic(float initialDuration) { super(initialDuration); } + /** + * Creates a cinematic that loops based on the provided loop mode. + * + * @param loopMode The loop mode. See {@link LoopMode}. + */ public Cinematic(LoopMode loopMode) { super(loopMode); } + /** + * Creates a cinematic with a specific duration and loop mode. + * + * @param initialDuration The total duration of the cinematic in seconds. + * @param loopMode The loop mode. See {@link LoopMode}. + */ public Cinematic(float initialDuration, LoopMode loopMode) { super(initialDuration, loopMode); } @@ -211,6 +236,41 @@ public void onPause() { } } + // HACK: null set the composer and cinematic references + // this is used to make this appstate deserializable without + // breaking the scene graph + private void applySerializationHack(CinematicEvent event, Map map) { + Object store[] = null; + if (event instanceof AbstractCinematicEvent) { + store = map.computeIfAbsent(event, k -> new Object[2]); + store[0] = ((AbstractCinematicEvent) event).getCinematic(); + ((AbstractCinematicEvent) event).setCinematic(null); + } + if (event instanceof AnimEvent) { + AnimEvent animEvent = (AnimEvent) event; + store[1] = animEvent.getComposer(); + + // set ref id that will be used to relink the composer after deserialization + String refId = animEvent.getAnimRef(); + AnimComposer composer = animEvent.getComposer(); + setModelRefId(composer.getSpatial(), refId); + + animEvent.setComposer(null); + } + } + + private void undoSerializationHack(CinematicEvent event, Map map) { + Object store[] = map.get(event); + if (store == null) return; + if (event instanceof AbstractCinematicEvent) { + ((AbstractCinematicEvent) event).setCinematic((CinematicHandler) store[0]); + } + if (event instanceof AnimEvent) { + AnimEvent animEvent = (AnimEvent) event; + animEvent.setComposer((AnimComposer) store[1]); + } + } + /** * used internally for serialization * @@ -221,10 +281,26 @@ public void onPause() { 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); + + Map tmp = new HashMap<>(); + + try { + + // hack + for (CinematicEvent event : cinematicEvents) { + applySerializationHack(event, tmp); + } + + oc.write(cinematicEvents.toArray(new CinematicEvent[cinematicEvents.size()]), "cinematicEvents", + null); oc.writeStringSavableMap(cameras, "cameras", null); oc.write(timeLine, "timeLine", null); - + } finally { + // unhack + for (CinematicEvent event : cinematicEvents) { + undoSerializationHack(event, tmp); + } + } } /** @@ -241,7 +317,7 @@ public void read(JmeImporter im) throws IOException { Savable[] events = ic.readSavableArray("cinematicEvents", null); for (Savable c : events) { -// addCinematicEvent(((CinematicEvent) c).getTime(), (CinematicEvent) c) + relinkCinematic((CinematicEvent) c); cinematicEvents.add((CinematicEvent) c); } cameras = (Map) ic.readStringSavableMap("cameras", null); @@ -273,10 +349,14 @@ public void setSpeed(float speed) { */ @Override public void initialize(AppStateManager stateManager, Application app) { + this.app = app; initEvent(app, this); for (CinematicEvent cinematicEvent : cinematicEvents) { + relinkAnimComposer(cinematicEvent); + relinkCinematic(cinematicEvent); cinematicEvent.initEvent(app, this); } + if (!cameras.isEmpty()) { for (CameraNode n : cameras.values()) { n.setCamera(app.getCamera()); @@ -285,6 +365,52 @@ public void initialize(AppStateManager stateManager, Application app) { initialized = true; } + private void relinkAnimComposer(CinematicEvent cinematicEvent) { + if (cinematicEvent instanceof AnimEvent) { + AnimEvent animEvent = (AnimEvent) cinematicEvent; + AnimComposer composer = animEvent.getComposer(); + if (composer == null) { + String ref = animEvent.getAnimRef(); + Spatial sp = findModelByRef(scene, ref); + if (sp == null) { + throw new IllegalStateException( + "Cannot find model with ref id " + ref + " for AnimEvent"); + } + composer = sp.getControl(AnimComposer.class); + animEvent.setComposer(composer); + } + } + } + + private void relinkCinematic(CinematicEvent cinematicEvent) { + if (cinematicEvent instanceof AbstractCinematicEvent) { + AbstractCinematicEvent ace = (AbstractCinematicEvent) cinematicEvent; + ace.setCinematic(this); + } + } + + private Spatial findModelByRef(Spatial sp, String spatialRef) { + String keyToSearch = CINEMATIC_REF_PREFIX + spatialRef; + Object refObj = sp.getUserData(keyToSearch); + if (CINEMATIC_REF_VALUE.equals(refObj)) { + return sp; + } + if (sp instanceof Node) { + for (Spatial child : ((Node) sp).getChildren()) { + Spatial model = findModelByRef(child, spatialRef); + if (model != null) { + return model; + } + } + } + return null; + } + + private void setModelRefId(Spatial sp, String spatialRef) { + String key = CINEMATIC_REF_PREFIX + spatialRef; + sp.setUserData(key, CINEMATIC_REF_VALUE); + } + /** * used internally * @@ -342,6 +468,11 @@ public boolean isEnabled() { */ @Override public void stateAttached(AppStateManager stateManager) { + for (CameraNode n : cameras.values()) { + if (n.getParent() == null) { + scene.attachChild(n); + } + } } /** @@ -352,6 +483,12 @@ public void stateAttached(AppStateManager stateManager) { @Override public void stateDetached(AppStateManager stateManager) { stop(); + + for (CameraNode n : cameras.values()) { + if (n.getParent() != null) { + scene.detachChild(n); + } + } } /** @@ -434,6 +571,7 @@ public void setTime(float time) { * @param cinematicEvent the cinematic event * @return the keyFrame for that event. */ + @Override public KeyFrame addCinematicEvent(float timeStamp, CinematicEvent cinematicEvent) { KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp); if (keyFrame == null) { @@ -443,7 +581,7 @@ public KeyFrame addCinematicEvent(float timeStamp, CinematicEvent cinematicEvent keyFrame.cinematicEvents.add(cinematicEvent); cinematicEvents.add(cinematicEvent); if (isInitialized()) { - cinematicEvent.initEvent(null, this); + cinematicEvent.initEvent(app, this); } return keyFrame; } @@ -455,6 +593,7 @@ public KeyFrame addCinematicEvent(float timeStamp, CinematicEvent cinematicEvent * @param cinematicEvent the cinematic event to enqueue * @return the timestamp the event was scheduled. */ + @Override public float enqueueCinematicEvent(CinematicEvent cinematicEvent) { float scheduleTime = nextEnqueue; addCinematicEvent(scheduleTime, cinematicEvent); @@ -468,6 +607,7 @@ public float enqueueCinematicEvent(CinematicEvent cinematicEvent) { * @param cinematicEvent the cinematicEvent to remove * @return true if the element has been removed */ + @Override public boolean removeCinematicEvent(CinematicEvent cinematicEvent) { cinematicEvent.dispose(); cinematicEvents.remove(cinematicEvent); @@ -487,8 +627,8 @@ public boolean removeCinematicEvent(CinematicEvent cinematicEvent) { * @param cinematicEvent the cinematicEvent to remove * @return true if the element has been removed */ + @Override public boolean removeCinematicEvent(float timeStamp, CinematicEvent cinematicEvent) { - cinematicEvent.dispose(); KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp); return removeCinematicEvent(keyFrame, cinematicEvent); } @@ -501,6 +641,7 @@ public boolean removeCinematicEvent(float timeStamp, CinematicEvent cinematicEve * @param cinematicEvent the cinematicEvent to remove * @return true if the element has been removed */ + @Override public boolean removeCinematicEvent(KeyFrame keyFrame, CinematicEvent cinematicEvent) { cinematicEvent.dispose(); boolean ret = keyFrame.cinematicEvents.remove(cinematicEvent); @@ -536,6 +677,7 @@ public void postRender() { */ @Override public void cleanup() { + initialized = false; } /** @@ -586,6 +728,7 @@ public CameraNode bindCamera(String cameraName, Camera cam) { * Cinematic#bindCamera()) * @return the cameraNode for this name */ + @Override public CameraNode getCamera(String cameraName) { return cameras.get(cameraName); } @@ -608,6 +751,7 @@ private void setEnableCurrentCam(boolean enabled) { * @param cameraName the camera name (as registered in * Cinematic#bindCamera()) */ + @Override public void setActiveCamera(String cameraName) { setEnableCurrentCam(false); currentCam = cameras.get(cameraName); @@ -624,6 +768,7 @@ public void setActiveCamera(String cameraName) { * @param cameraName the camera name (as registered in * Cinematic#bindCamera()) */ + @Override public void activateCamera(final float timeStamp, final String cameraName) { addCinematicEvent(timeStamp, new CameraEvent(this, cameraName)); } @@ -647,6 +792,7 @@ private Map> getEventsData() { * @param key the key * @param object the data */ + @Override public void putEventData(String type, Object key, Object object) { Map> data = getEventsData(); Map row = data.get(type); @@ -664,6 +810,7 @@ public void putEventData(String type, Object key, Object object) { * @param key the key * @return the pre-existing object, or null */ + @Override public Object getEventData(String type, Object key) { if (eventsData != null) { Map row = eventsData.get(type); @@ -680,6 +827,7 @@ public Object getEventData(String type, Object key) { * @param type the type of data * @param key the key of the data */ + @Override public void removeEventData(String type, Object key) { if (eventsData != null) { Map row = eventsData.get(type); @@ -713,6 +861,7 @@ public Node getScene() { return scene; } + /** * Remove all events from the Cinematic. */ @@ -725,6 +874,17 @@ public void clear() { } } + /** + * Clears all camera nodes bound to the cinematic from the scene node. This method removes all previously + * bound CameraNodes and clears the internal camera map, effectively detaching all cameras from the scene. + */ + public void clearCameras() { + for (CameraNode cameraNode : cameras.values()) { + scene.detachChild(cameraNode); + } + cameras.clear(); + } + /** * used internally to clean up the cinematic. Called when the clear() method * is called @@ -735,4 +895,9 @@ public void dispose() { event.dispose(); } } + + @Override + public AssetManager getAssetManager() { + return app.getAssetManager(); + } } diff --git a/jme3-core/src/main/java/com/jme3/cinematic/CinematicBase.java b/jme3-core/src/main/java/com/jme3/cinematic/CinematicBase.java new file mode 100644 index 0000000000..0071edea8e --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/cinematic/CinematicBase.java @@ -0,0 +1,104 @@ +package com.jme3.cinematic; + +import com.jme3.animation.LoopMode; + +/** + * The base interface for cinematic. + */ +public interface CinematicBase { + + /** + * Starts the animation + */ + public void play(); + + /** + * Stops the animation + */ + public void stop(); + + /** + * this method can be implemented if the event needs different handling when + * stopped naturally (when the event reach its end) + * or when it was forced stopped during playback + * otherwise it just calls regular stop() + */ + public void forceStop(); + + /** + * Pauses the animation + */ + public void pause(); + + /** + * Returns the actual duration of the animation + * @return the duration + */ + public float getDuration(); + + /** + * Sets the speed of the animation (1 is normal speed, 2 is twice faster) + * + * @param speed the desired speed (default=1) + */ + public void setSpeed(float speed); + + /** + * returns the speed of the animation + * @return the speed + */ + public float getSpeed(); + + /** + * returns the PlayState of the animation + * @return the plat state + */ + public PlayState getPlayState(); + + /** + * @param loop 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 loop); + + /** + * @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 + */ + public LoopMode getLoopMode(); + + /** + * returns the initial duration of the animation at speed = 1 in seconds. + * @return the initial duration + */ + public float getInitialDuration(); + + /** + * Sets the duration of the animation at speed = 1, in seconds. + * + * @param initialDuration the desired duration (in de-scaled seconds) + */ + public void setInitialDuration(float initialDuration); + + /** + * Fast-forwards to the given time, where time=0 is the start of the event. + * + * @param time the time to fast-forward to + */ + public void setTime(float time); + + /** + * returns the current time of the cinematic event + * @return the time + */ + public float getTime(); + +} diff --git a/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java b/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java new file mode 100644 index 0000000000..d93fe8626c --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java @@ -0,0 +1,120 @@ +package com.jme3.cinematic; + +import com.jme3.asset.AssetManager; +import com.jme3.cinematic.events.CinematicEvent; +import com.jme3.export.Savable; +import com.jme3.scene.CameraNode; + +/** + * A interface that defines an object that can compose and coordinate cinematic events. + */ +public interface CinematicHandler extends CinematicBase, Savable { + + /** + * 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. + */ + KeyFrame addCinematicEvent(float timeStamp, CinematicEvent cinematicEvent); + + /** + * Enqueue a cinematic event to a Cinematic. This is handy when you + * want to chain events without knowing their durations. + * + * @param cinematicEvent the cinematic event to enqueue + * @return the timestamp the event was scheduled. + */ + float enqueueCinematicEvent(CinematicEvent cinematicEvent); + + /** + * removes the first occurrence found of the given cinematicEvent. + * + * @param cinematicEvent the cinematicEvent to remove + * @return true if the element has been removed + */ + boolean removeCinematicEvent(CinematicEvent cinematicEvent); + + /** + * 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 + */ + boolean removeCinematicEvent(float timeStamp, CinematicEvent 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 + */ + boolean removeCinematicEvent(KeyFrame keyFrame, CinematicEvent cinematicEvent); + + /** + * returns a cameraNode given its name + * + * @param cameraName the camera name (as registered in + * Cinematic#bindCamera()) + * @return the cameraNode for this name + */ + CameraNode getCamera(String cameraName); + + /** + * Sets the active camera instantly (use activateCamera if you want to + * schedule that event) + * + * @param cameraName the camera name (as registered in + * Cinematic#bindCamera()) + */ + void setActiveCamera(String cameraName); + + /** + * 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()) + */ + void activateCamera(float timeStamp, String cameraName); + + /** + * used internally put an eventdata in the cinematic + * + * @param type the type of data + * @param key the key + * @param object the data + */ + void putEventData(String type, Object key, Object object); + + /** + * used internally return and event data + * + * @param type the type of data + * @param key the key + * @return the pre-existing object, or null + */ + Object getEventData(String type, Object key); + + /** + * Used internally remove an eventData + * + * @param type the type of data + * @param key the key of the data + */ + void removeEventData(String type, Object key); + + /** + * Returns the AssetManager associated with this CinematicHandler. + * + * @return The AssetManager instance. + */ + AssetManager getAssetManager(); +} \ No newline at end of file diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/AbstractCinematicEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/AbstractCinematicEvent.java index 85bae7c857..d4ef0802f2 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/events/AbstractCinematicEvent.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/events/AbstractCinematicEvent.java @@ -33,8 +33,7 @@ import com.jme3.animation.AnimationUtils; import com.jme3.animation.LoopMode; -import com.jme3.app.Application; -import com.jme3.cinematic.Cinematic; +import com.jme3.cinematic.CinematicHandler; import com.jme3.cinematic.PlayState; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; @@ -60,6 +59,7 @@ public abstract class AbstractCinematicEvent implements CinematicEvent { protected float speed = 1; protected float time = 0; protected boolean resuming = false; + protected CinematicHandler cinematic; /** * The list of listeners. @@ -294,6 +294,7 @@ public void write(JmeExporter ex) throws IOException { oc.write(speed, "speed", 1); oc.write(initialDuration, "initalDuration", 10); oc.write(loopMode, "loopMode", LoopMode.DontLoop); + oc.write(cinematic, "cinematic", null); } /** @@ -308,6 +309,7 @@ public void read(JmeImporter im) throws IOException { speed = ic.readFloat("speed", 1); initialDuration = ic.readFloat("initalDuration", 10); loopMode = ic.readEnum("loopMode", LoopMode.class, LoopMode.DontLoop); + cinematic = (CinematicHandler) ic.readSavable("cinematic", null); } /** @@ -317,7 +319,8 @@ public void read(JmeImporter im) throws IOException { * @param cinematic ignored */ @Override - public void initEvent(Application app, Cinematic cinematic) { + public void initEvent(CinematicHandler cinematic) { + this.cinematic = cinematic; } /** @@ -370,4 +373,13 @@ public float getTime() { @Override public void dispose() { } + + public CinematicHandler getCinematic() { + return cinematic; + } + + public void setCinematic(CinematicHandler cinematic) { + this.cinematic = cinematic; + } + } diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/AnimEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/AnimEvent.java index 7d7721e166..348a8399ac 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/events/AnimEvent.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/events/AnimEvent.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2021 jMonkeyEngine + * Copyright (c) 2009-2026 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -34,14 +34,14 @@ import com.jme3.anim.AnimComposer; import com.jme3.anim.tween.action.Action; import com.jme3.animation.LoopMode; -import com.jme3.app.Application; -import com.jme3.cinematic.Cinematic; +import com.jme3.cinematic.CinematicHandler; 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 java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; @@ -53,17 +53,20 @@ */ public class AnimEvent extends AbstractCinematicEvent { - public static final Logger logger + private static final Logger logger = Logger.getLogger(AnimEvent.class.getName()); + + private static final AtomicLong refCounter = new AtomicLong(); + + /** + * Unique-ish id that identify this anim event + */ + protected String animRef; /* * Control that will play the animation */ private AnimComposer composer; - /* - * Cinematic that contains this event - */ - private Cinematic cinematic; /* * name of the animation action to be played */ @@ -73,6 +76,16 @@ public class AnimEvent extends AbstractCinematicEvent { */ private String layerName; + /** + * Constructs a new AnimEvent to play the named action on the default layer. + * + * @param composer the Control that will play the animation (not null). + * @param actionName the name of the animation action to play. + */ + public AnimEvent(AnimComposer composer, String actionName) { + this(composer, actionName, AnimComposer.DEFAULT_LAYER); + } + /** * Instantiate a non-looping event to play the named action on the named * layer of the specified AnimComposer. @@ -84,6 +97,7 @@ public class AnimEvent extends AbstractCinematicEvent { */ public AnimEvent(AnimComposer composer, String actionName, String layerName) { + this(); this.composer = composer; this.actionName = actionName; this.layerName = layerName; @@ -99,6 +113,7 @@ public AnimEvent(AnimComposer composer, String actionName, */ protected AnimEvent() { super(); + animRef = "animEvent-" + System.currentTimeMillis() + "_" + refCounter.incrementAndGet(); } /** @@ -108,9 +123,8 @@ protected AnimEvent() { * @param cinematic the Cinematic that contains this event */ @Override - public void initEvent(Application app, Cinematic cinematic) { - super.initEvent(app, cinematic); - this.cinematic = cinematic; + public void initEvent(CinematicHandler cinematic) { + super.initEvent(cinematic); } /** @@ -191,11 +205,10 @@ public void onUpdate(float tpf) { public void read(JmeImporter importer) throws IOException { super.read(importer); InputCapsule capsule = importer.getCapsule(this); - actionName = capsule.readString("actionName", ""); - cinematic = (Cinematic) capsule.readSavable("cinematic", null); composer = (AnimComposer) capsule.readSavable("composer", null); layerName = capsule.readString("layerName", AnimComposer.DEFAULT_LAYER); + animRef = capsule.readString("animRef", null); } /** @@ -269,10 +282,21 @@ public void setTime(float time) { public void write(JmeExporter exporter) throws IOException { super.write(exporter); OutputCapsule capsule = exporter.getCapsule(this); - capsule.write(actionName, "actionName", ""); - capsule.write(cinematic, "cinematic", null); capsule.write(composer, "composer", null); capsule.write(layerName, "layerName", AnimComposer.DEFAULT_LAYER); + capsule.write(animRef, "animRef", null); + } + + public AnimComposer getComposer() { + return composer; + } + + public void setComposer(AnimComposer composer) { + this.composer = composer; + } + + public String getAnimRef() { + return animRef; } } diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java index 6511da701b..be3cfe5713 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/events/AnimationEvent.java @@ -32,8 +32,8 @@ package com.jme3.cinematic.events; import com.jme3.animation.*; -import com.jme3.app.Application; import com.jme3.cinematic.Cinematic; +import com.jme3.cinematic.CinematicHandler; import com.jme3.cinematic.PlayState; import com.jme3.export.*; import com.jme3.scene.Node; @@ -287,9 +287,9 @@ public AnimationEvent(Spatial model, String animationName, float initialDuration @Override @SuppressWarnings("unchecked") - public void initEvent(Application app, Cinematic cinematic) { - super.initEvent(app, cinematic); - this.cinematic = cinematic; + public void initEvent(CinematicHandler handler) { + super.initEvent(handler); + this.cinematic = (Cinematic) handler; if (channel == null) { Object s = cinematic.getEventData(MODEL_CHANNELS, model); if (s == null) { diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java index a9dfec3960..7168ad93f5 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/events/CameraEvent.java @@ -31,8 +31,7 @@ */ package com.jme3.cinematic.events; -import com.jme3.app.Application; -import com.jme3.cinematic.Cinematic; +import com.jme3.cinematic.CinematicHandler; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; @@ -51,11 +50,6 @@ public class CameraEvent extends AbstractCinematicEvent { * The name of the camera to activate. */ private String cameraName; - /** - * The `Cinematic` instance to which this event belongs and on which the - * camera will be set. - */ - private Cinematic cinematic; /** * For serialization only. Do not use. @@ -69,15 +63,14 @@ public CameraEvent() { * @param cinematic The `Cinematic` instance this event belongs to (cannot be null). * @param cameraName The name of the camera to be activated by this event (cannot be null or empty). */ - public CameraEvent(Cinematic cinematic, String cameraName) { - this.cinematic = cinematic; + public CameraEvent(CinematicHandler cinematic, String cameraName) { this.cameraName = cameraName; + setCinematic(cinematic); } @Override - public void initEvent(Application app, Cinematic cinematic) { - super.initEvent(app, cinematic); - this.cinematic = cinematic; + public void initEvent(CinematicHandler cinematic) { + super.initEvent(cinematic); } @Override @@ -88,7 +81,7 @@ public void play() { @Override public void onPlay() { - cinematic.setActiveCamera(cameraName); + getCinematic().setActiveCamera(cameraName); } @Override @@ -116,18 +109,10 @@ public void setTime(float time) { * Returns the `Cinematic` instance associated with this event. * @return The `Cinematic` instance. */ - public Cinematic getCinematic() { + public CinematicHandler getCinematic() { return cinematic; } - /** - * Sets the `Cinematic` instance for this event. - * @param cinematic The `Cinematic` instance to set (cannot be null). - */ - public void setCinematic(Cinematic cinematic) { - this.cinematic = cinematic; - } - /** * Returns the name of the camera that this event will activate. * @return The camera name. diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/CinematicEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/CinematicEvent.java index 4515ca8a3a..c99c1632a5 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/events/CinematicEvent.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/events/CinematicEvent.java @@ -31,98 +31,17 @@ */ package com.jme3.cinematic.events; -import com.jme3.animation.LoopMode; import com.jme3.app.Application; import com.jme3.cinematic.Cinematic; -import com.jme3.cinematic.PlayState; +import com.jme3.cinematic.CinematicHandler; +import com.jme3.cinematic.CinematicBase; import com.jme3.export.Savable; /** * * @author Nehon */ -public interface CinematicEvent extends Savable { - - /** - * Starts the animation - */ - public void play(); - - /** - * Stops the animation - */ - public void stop(); - - /** - * this method can be implemented if the event needs different handling when - * stopped naturally (when the event reach its end) - * or when it was forced stopped during playback - * otherwise it just calls regular stop() - */ - public void forceStop(); - - /** - * Pauses the animation - */ - public void pause(); - - /** - * Returns the actual duration of the animation - * @return the duration - */ - public float getDuration(); - - /** - * Sets the speed of the animation (1 is normal speed, 2 is twice faster) - * - * @param speed the desired speed (default=1) - */ - public void setSpeed(float speed); - - /** - * returns the speed of the animation - * @return the speed - */ - public float getSpeed(); - - /** - * returns the PlayState of the animation - * @return the plat state - */ - public PlayState getPlayState(); - - /** - * @param loop 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 loop); - - /** - * @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 - */ - public LoopMode getLoopMode(); - - /** - * returns the initial duration of the animation at speed = 1 in seconds. - * @return the initial duration - */ - public float getInitialDuration(); - - /** - * Sets the duration of the animation at speed = 1, in seconds. - * - * @param initialDuration the desired duration (in de-scaled seconds) - */ - public void setInitialDuration(float initialDuration); +public interface CinematicEvent extends Savable, CinematicBase { /** * called internally in the update method, place here anything you want to run in the update loop @@ -135,20 +54,12 @@ public interface CinematicEvent extends Savable { * @param app the application * @param cinematic the cinematic */ - public void initEvent(Application app, Cinematic cinematic); - - /** - * Fast-forwards to the given time, where time=0 is the start of the event. - * - * @param time the time to fast-forward to - */ - public void setTime(float time); - - /** - * returns the current time of the cinematic event - * @return the time - */ - public float getTime(); + public void initEvent(CinematicHandler cinematic); + + @Deprecated + public default void initEvent(Application app, Cinematic cinematic) { + initEvent((CinematicHandler) cinematic); + } /** * method called when an event is removed from a cinematic diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java index d4751807ef..ccf789a5ae 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java @@ -34,7 +34,7 @@ import com.jme3.animation.AnimationUtils; import com.jme3.animation.LoopMode; import com.jme3.app.Application; -import com.jme3.cinematic.Cinematic; +import com.jme3.cinematic.CinematicHandler; import com.jme3.cinematic.MotionPath; import com.jme3.cinematic.PlayState; import com.jme3.export.InputCapsule; @@ -197,8 +197,8 @@ public void internalUpdate(float tpf) { } @Override - public void initEvent(Application app, Cinematic cinematic) { - super.initEvent(app, cinematic); + public void initEvent(CinematicHandler cinematic) { + super.initEvent(cinematic); isControl = false; } diff --git a/jme3-core/src/main/java/com/jme3/cinematic/events/SoundEvent.java b/jme3-core/src/main/java/com/jme3/cinematic/events/SoundEvent.java index b899781bbe..b319de2c4b 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/events/SoundEvent.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/events/SoundEvent.java @@ -32,11 +32,10 @@ package com.jme3.cinematic.events; import com.jme3.animation.LoopMode; -import com.jme3.app.Application; import com.jme3.audio.AudioData; import com.jme3.audio.AudioNode; import com.jme3.audio.AudioSource; -import com.jme3.cinematic.Cinematic; +import com.jme3.cinematic.CinematicHandler; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; @@ -152,9 +151,10 @@ public SoundEvent() { } @Override - public void initEvent(Application app, Cinematic cinematic) { - super.initEvent(app, cinematic); - audioNode = new AudioNode(app.getAssetManager(), path, stream ? AudioData.DataType.Stream : AudioData.DataType.Buffer); + public void initEvent(CinematicHandler cinematic) { + super.initEvent(cinematic); + audioNode = new AudioNode(cinematic.getAssetManager(), path, + stream ? AudioData.DataType.Stream : AudioData.DataType.Buffer); audioNode.setPositional(false); setLoopMode(loopMode); } @@ -217,6 +217,7 @@ public void write(JmeExporter ex) throws IOException { OutputCapsule oc = ex.getCapsule(this); oc.write(path, "path", ""); oc.write(stream, "stream", false); + oc.write(audioNode, "audioNode", null); } @Override @@ -225,5 +226,6 @@ public void read(JmeImporter im) throws IOException { InputCapsule ic = im.getCapsule(this); path = ic.readString("path", ""); stream = ic.readBoolean("stream", false); + audioNode = (AudioNode) ic.readSavable("audioNode", null); } } diff --git a/jme3-examples/src/main/java/jme3test/animation/TestAnimEventSavable.java b/jme3-examples/src/main/java/jme3test/animation/TestAnimEventSavable.java new file mode 100644 index 0000000000..b184c5da96 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/animation/TestAnimEventSavable.java @@ -0,0 +1,147 @@ +package jme3test.animation; + +import java.io.IOException; + +import com.jme3.anim.AnimClip; +import com.jme3.anim.AnimComposer; +import com.jme3.anim.AnimFactory; +import com.jme3.anim.util.AnimMigrationUtils; +import com.jme3.app.SimpleApplication; +import com.jme3.cinematic.PlayState; +import com.jme3.cinematic.events.AnimEvent; +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.export.binary.BinaryExporter; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Caps; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.shadow.DirectionalLightShadowRenderer; + + +public class TestAnimEventSavable extends SimpleApplication { + + public static void main(String[] args) { + TestAnimEventSavable app = new TestAnimEventSavable(); + app.setPauseOnLostFocus(false); + app.start(); + } + + private Spatial teapot; + private AnimEvent evt; + + public static class AnimatedScene implements Savable{ + public Node scene; + public AnimEvent anim; + + @Override + public void write(JmeExporter ex) throws IOException { + OutputCapsule oc = ex.getCapsule(this); + oc.write(scene, "scene", null); + oc.write(anim, "anim", null); + } + + @Override + public void read(JmeImporter im) throws IOException { + InputCapsule ic = im.getCapsule(this); + scene = (Node) ic.readSavable("scene", null); + anim = (AnimEvent) ic.readSavable("anim", null); + } + + } + + @Override + public void simpleInitApp() { + + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + + + setupLightsAndFilters(); + setupModel(); + + Node jaime = (Node) assetManager.loadModel("Models/Jaime/Jaime.j3o"); + AnimMigrationUtils.migrate(jaime); + jaime.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + evt = new AnimEvent(jaime.getControl(AnimComposer.class), "JumpStart", AnimComposer.DEFAULT_LAYER); + + AnimatedScene original = new AnimatedScene(); + original.scene = jaime; + original.anim = evt; + + AnimatedScene copy = BinaryExporter.saveAndLoad(assetManager, original); + rootNode.attachChild(copy.scene); + evt = copy.anim; + + assert copy.anim.getComposer()!=original.anim.getComposer(); + assert copy.scene.getControl(AnimComposer.class)==copy.anim.getComposer(); + + + initInputs(); + } + + + private void setupLightsAndFilters() { + DirectionalLight light = new DirectionalLight(); + light.setDirection(new Vector3f(0, -1, -1).normalizeLocal()); + light.setColor(ColorRGBA.White.mult(1.5f)); + rootNode.addLight(light); + + if (renderer.getCaps().contains(Caps.GLSL100)) { + DirectionalLightShadowRenderer dlsr = new DirectionalLightShadowRenderer(assetManager, 512, 1); + dlsr.setLight(light); + dlsr.setShadowIntensity(0.4f); + viewPort.addProcessor(dlsr); + } + } + + private void setupModel() { + teapot = assetManager.loadModel("Models/Teapot/Teapot.obj"); + teapot.setLocalTranslation(5, 0, 5); + Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", ColorRGBA.Cyan); + teapot.setMaterial(mat); + teapot.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + rootNode.attachChild(teapot); + + // creating spatial animation for the teapot + AnimFactory factory = new AnimFactory(20f, "teapotAnim", 30f); + factory.addTimeTranslation(0, new Vector3f(5, 0, 5)); + factory.addTimeTranslation(4, new Vector3f(5, 0, -5)); + AnimClip animClip = factory.buildAnimation(teapot); + + AnimComposer animComposer = new AnimComposer(); + animComposer.addAnimClip(animClip); + teapot.addControl(animComposer); + } + + + private void initInputs() { + inputManager.addMapping("togglePlay", new KeyTrigger(KeyInput.KEY_SPACE)); + ActionListener acl = new ActionListener() { + + @Override + public void onAction(String name, boolean keyPressed, float tpf) { + if (name.equals("togglePlay") && keyPressed) { + if (evt.getPlayState() == PlayState.Playing) { + evt.pause(); + } else { + System.out.println("Play"); + evt.play(); + } + } + + } + }; + inputManager.addListener(acl, "togglePlay"); + } +} \ No newline at end of file diff --git a/jme3-examples/src/main/java/jme3test/animation/TestCinematicSavable.java b/jme3-examples/src/main/java/jme3test/animation/TestCinematicSavable.java new file mode 100644 index 0000000000..ee51fc1b6e --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/animation/TestCinematicSavable.java @@ -0,0 +1,188 @@ +package jme3test.animation; + +import com.jme3.anim.AnimClip; +import com.jme3.anim.AnimComposer; +import com.jme3.anim.AnimFactory; +import com.jme3.anim.util.AnimMigrationUtils; +import com.jme3.animation.LoopMode; +import com.jme3.app.SimpleApplication; +import com.jme3.cinematic.Cinematic; +import com.jme3.cinematic.MotionPath; +import com.jme3.cinematic.PlayState; +import com.jme3.cinematic.events.AnimEvent; +import com.jme3.cinematic.events.CinematicEvent; +import com.jme3.cinematic.events.CinematicEventListener; +import com.jme3.cinematic.events.MotionEvent; +import com.jme3.cinematic.events.SoundEvent; +import com.jme3.export.binary.BinaryExporter; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.light.DirectionalLight; +import com.jme3.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Vector3f; +import com.jme3.renderer.Caps; +import com.jme3.renderer.queue.RenderQueue; +import com.jme3.scene.CameraNode; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.shadow.DirectionalLightShadowRenderer; + +/** + * + * @author capdevon + */ +public class TestCinematicSavable extends SimpleApplication { + + public static void main(String[] args) { + TestCinematicSavable app = new TestCinematicSavable(); + app.setPauseOnLostFocus(false); + app.start(); + } + + private Cinematic cinematic; + private Spatial teapot; + private MotionEvent cameraMotionEvent; + + @Override + public void simpleInitApp() { + + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + + cinematic = new Cinematic(rootNode, 10); + + + setupLightsAndFilters(); + setupModel(); + createCameraMotion(); + + Node jaime = (Node) assetManager.loadModel("Models/Jaime/Jaime.j3o"); + AnimMigrationUtils.migrate(jaime); + jaime.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + rootNode.attachChild(jaime); + + cinematic.activateCamera(0, "aroundCam"); + // cinematic.activateCamera(0, "topView"); + cinematic.addCinematicEvent(0f, new AnimEvent(teapot.getControl(AnimComposer.class), "teapotAnim", + AnimComposer.DEFAULT_LAYER)); + cinematic.addCinematicEvent(0f, + new AnimEvent(jaime.getControl(AnimComposer.class), "JumpStart", AnimComposer.DEFAULT_LAYER)); + cinematic.addCinematicEvent(0f, cameraMotionEvent); + cinematic.addCinematicEvent(0f, new SoundEvent("Sound/Environment/Nature.ogg", LoopMode.Loop)); + cinematic.addCinematicEvent(3f, new SoundEvent("Sound/Effects/kick.wav")); + cinematic.addCinematicEvent(5.1f, new SoundEvent("Sound/Effects/Beep.ogg", 1)); + + cinematic.addListener(new CinematicEventListener() { + + @Override + public void onPlay(CinematicEvent cinematic) { + flyCam.setEnabled(false); + System.out.println("play"); + } + + @Override + public void onPause(CinematicEvent cinematic) { + System.out.println("pause"); + } + + @Override + public void onStop(CinematicEvent cinematic) { + flyCam.setEnabled(true); + System.out.println("stop"); + } + }); + + Cinematic copy = BinaryExporter.saveAndLoad(assetManager, cinematic); + + cinematic = copy; + cinematic.setScene(rootNode); + stateManager.attach(cinematic); + + configureCamera(); + + initInputs(); + } + + private void configureCamera() { + flyCam.setMoveSpeed(25f); + flyCam.setDragToRotate(true); + cam.setLocation(Vector3f.UNIT_XYZ.mult(12)); + cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y); + } + + private void setupLightsAndFilters() { + DirectionalLight light = new DirectionalLight(); + light.setDirection(new Vector3f(0, -1, -1).normalizeLocal()); + light.setColor(ColorRGBA.White.mult(1.5f)); + rootNode.addLight(light); + + if (renderer.getCaps().contains(Caps.GLSL100)) { + DirectionalLightShadowRenderer dlsr = new DirectionalLightShadowRenderer(assetManager, 512, 1); + dlsr.setLight(light); + dlsr.setShadowIntensity(0.4f); + viewPort.addProcessor(dlsr); + } + } + + private void setupModel() { + teapot = assetManager.loadModel("Models/Teapot/Teapot.obj"); + teapot.setLocalTranslation(5, 0, 5); + Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md"); + mat.setColor("Color", ColorRGBA.Cyan); + teapot.setMaterial(mat); + teapot.setShadowMode(RenderQueue.ShadowMode.CastAndReceive); + rootNode.attachChild(teapot); + + // creating spatial animation for the teapot + AnimFactory factory = new AnimFactory(20f, "teapotAnim", 30f); + factory.addTimeTranslation(0, new Vector3f(5, 0, 5)); + factory.addTimeTranslation(4, new Vector3f(5, 0, -5)); + AnimClip animClip = factory.buildAnimation(teapot); + + AnimComposer animComposer = new AnimComposer(); + animComposer.addAnimClip(animClip); + teapot.addControl(animComposer); + } + + private void createCameraMotion() { + CameraNode camNode = cinematic.bindCamera("topView", cam); + camNode.setLocalTranslation(new Vector3f(0, 50, 0)); + camNode.lookAt(teapot.getLocalTranslation(), Vector3f.UNIT_Y); + + CameraNode camNode2 = cinematic.bindCamera("aroundCam", cam); + MotionPath path = new MotionPath(); + path.setCycle(true); + path.addWayPoint(new Vector3f(20, 3, 0)); + path.addWayPoint(new Vector3f(0, 3, 20)); + path.addWayPoint(new Vector3f(-20, 3, 0)); + path.addWayPoint(new Vector3f(0, 3, -20)); + path.setCurveTension(0.83f); + cameraMotionEvent = new MotionEvent(camNode2, path); + cameraMotionEvent.setLoopMode(LoopMode.Loop); + cameraMotionEvent.setLookAt(teapot.getWorldTranslation(), Vector3f.UNIT_Y); + cameraMotionEvent.setDirectionType(MotionEvent.Direction.LookAt); + } + + private void initInputs() { + inputManager.addMapping("togglePlay", new KeyTrigger(KeyInput.KEY_SPACE)); + ActionListener acl = new ActionListener() { + + @Override + public void onAction(String name, boolean keyPressed, float tpf) { + if (name.equals("togglePlay") && keyPressed) { + Cinematic cinematic = stateManager.getState(Cinematic.class); + System.out.println("Toggle Play/Pause on cinematic: " + cinematic); + if (cinematic.getPlayState() == PlayState.Playing) { + cinematic.pause(); + } else { + System.out.println("Play"); + cinematic.play(); + } + } + + } + }; + inputManager.addListener(acl, "togglePlay"); + } +} \ No newline at end of file