From a0ac32d5244c30be778bec17d7066809e343a6f8 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Mon, 22 Sep 2025 14:41:07 +0200 Subject: [PATCH 01/18] AnimEvent: fix serialization bug --- .../com/jme3/cinematic/events/AnimEvent.java | 52 ++++++++++++++++--- 1 file changed, 46 insertions(+), 6 deletions(-) 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..a1a307fac2 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-2025 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -41,6 +41,9 @@ 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 java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; @@ -56,6 +59,7 @@ public class AnimEvent extends AbstractCinematicEvent { public static final Logger logger = Logger.getLogger(AnimEvent.class.getName()); + private Spatial model; /* * Control that will play the animation */ @@ -73,6 +77,17 @@ public class AnimEvent extends AbstractCinematicEvent { */ private String layerName; + /** + * Instantiate a non-looping event to play the named action on the default + * layer of the specified AnimComposer. + * + * @param composer the Control that will play the animation (not null) + * @param actionName the name of the animation action to be played + */ + 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 +99,7 @@ public class AnimEvent extends AbstractCinematicEvent { */ public AnimEvent(AnimComposer composer, String actionName, String layerName) { + this.model = composer.getSpatial(); this.composer = composer; this.actionName = actionName; this.layerName = layerName; @@ -111,6 +127,26 @@ protected AnimEvent() { public void initEvent(Application app, Cinematic cinematic) { super.initEvent(app, cinematic); this.cinematic = cinematic; + + if (composer == null) { + if (model != null) { + if (cinematic.getScene() != null) { + Spatial sceneModel = cinematic.getScene().getChild(model.getName()); + if (sceneModel != null) { + Node parent = sceneModel.getParent(); + parent.detachChild(sceneModel); + sceneModel = model; + parent.attachChild(sceneModel); + } else { + cinematic.getScene().attachChild(model); + } + } + composer = model.getControl(AnimComposer.class); + + } else { + throw new UnsupportedOperationException("model should not be null"); + } + } } /** @@ -180,6 +216,13 @@ public void onUpdate(float tpf) { // do nothing } + @Override + public void dispose() { + super.dispose(); + cinematic = null; + composer = null; + } + /** * De-serialize this event from the specified importer, for example when * loading from a J3O file. @@ -192,9 +235,8 @@ public void read(JmeImporter importer) throws IOException { super.read(importer); InputCapsule capsule = importer.getCapsule(this); + model = (Spatial) capsule.readSavable("model", null); actionName = capsule.readString("actionName", ""); - cinematic = (Cinematic) capsule.readSavable("cinematic", null); - composer = (AnimComposer) capsule.readSavable("composer", null); layerName = capsule.readString("layerName", AnimComposer.DEFAULT_LAYER); } @@ -269,10 +311,8 @@ public void setTime(float time) { public void write(JmeExporter exporter) throws IOException { super.write(exporter); OutputCapsule capsule = exporter.getCapsule(this); - + capsule.write(model, "model", null); capsule.write(actionName, "actionName", ""); - capsule.write(cinematic, "cinematic", null); - capsule.write(composer, "composer", null); capsule.write(layerName, "layerName", AnimComposer.DEFAULT_LAYER); } } From dc88115cc35de3182be0445da076c5ba4ac383a6 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Mon, 22 Sep 2025 14:42:05 +0200 Subject: [PATCH 02/18] Cinematic: fix initialization and serialization events --- .../java/com/jme3/cinematic/Cinematic.java | 116 ++++++++++++------ 1 file changed, 77 insertions(+), 39 deletions(-) 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..169362e6eb 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-2025 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -54,46 +54,50 @@ import java.util.logging.Logger; /** - * An appstate for composing and playing cutscenes in a game. The cinematic - * schedules CinematicEvents over a timeline. Once the Cinematic created it has - * to be attached to the stateManager. + * An AppState for composing and playing cutscenes in a game. * - * You can add various CinematicEvents to a Cinematic, see package - * com.jme3.cinematic.events + *

A cinematic schedules and plays {@link CinematicEvent}s over a timeline. + * Once a Cinematic is created, you must attach it to the `AppStateManager` to + * run it. You can add various `CinematicEvent`s, see the `com.jme3.cinematic.events` + * package for built-in event types. * - * Two main methods can be used to add an event : + *

Events can be added in two main ways: + *

* - * @see Cinematic#addCinematicEvent(float, - * com.jme3.cinematic.events.CinematicEvent) , that adds an event at the given - * time form the cinematic start. + *

Playback can be controlled with methods like: + *

* - * @see - * Cinematic#enqueueCinematicEvent(com.jme3.cinematic.events.CinematicEvent) - * that enqueue events one after the other according to their initialDuration + *

Since `Cinematic` itself extends `CinematicEvent`, you can nest cinematics + * within each other. Nested cinematics should not be attached to the `AppStateManager`. * - * A Cinematic has convenient methods to manage playback: - * @see Cinematic#play() - * @see Cinematic#pause() - * @see Cinematic#stop() - * - * A Cinematic is itself a CinematicEvent, meaning you can embed several - * cinematics. Embedded cinematics must not be added to the stateManager though. - * - * Cinematic can handle several points of view by creating camera nodes - * 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) + *

This class also handles multiple camera points of view by creating and + * activating camera nodes on a schedule. + *

* * @author Nehon */ public class Cinematic extends AbstractCinematicEvent implements AppState { private static final Logger logger = Logger.getLogger(Cinematic.class.getName()); + + private Application app; private Node scene; protected TimeLine timeLine = new TimeLine(); private int lastFetchedKeyFrame = -1; - private final List cinematicEvents = new ArrayList<>(); + private List cinematicEvents = new ArrayList<>(); private Map cameras = new HashMap<>(); private CameraNode currentCam; private boolean initialized = false; @@ -109,14 +113,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); } @@ -221,10 +241,9 @@ 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); + oc.writeSavableArrayList((ArrayList) cinematicEvents, "cinematicEvents", null); oc.writeStringSavableMap(cameras, "cameras", null); oc.write(timeLine, "timeLine", null); - } /** @@ -238,12 +257,7 @@ public void write(JmeExporter ex) throws IOException { 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); - } + cinematicEvents = ic.readSavableArrayList("cinematicEvents", null); cameras = (Map) ic.readStringSavableMap("cameras", null); timeLine = (TimeLine) ic.readSavable("timeLine", null); } @@ -273,6 +287,7 @@ public void setSpeed(float speed) { */ @Override public void initialize(AppStateManager stateManager, Application app) { + this.app = app; initEvent(app, this); for (CinematicEvent cinematicEvent : cinematicEvents) { cinematicEvent.initEvent(app, this); @@ -443,7 +458,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; } @@ -488,7 +503,6 @@ public boolean removeCinematicEvent(CinematicEvent cinematicEvent) { * @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); } @@ -536,6 +550,9 @@ public void postRender() { */ @Override public void cleanup() { + initialized = false; + clear(); + clearCameras(); } /** @@ -591,9 +608,9 @@ public CameraNode getCamera(String cameraName) { } /** - * enable/disable the camera control of the cameraNode of the current cam + * Enables or disables the camera control of the cameraNode of the current cam. * - * @param enabled + * @param enabled `true` to enable, `false` to disable. */ private void setEnableCurrentCam(boolean enabled) { if (currentCam != null) { @@ -713,6 +730,15 @@ public Node getScene() { return scene; } + /** + * Gets the application instance associated with this cinematic. + * + * @return The application. + */ + public Application getApplication() { + return app; + } + /** * Remove all events from the Cinematic. */ @@ -725,6 +751,18 @@ 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 From 1f06ea3e11e6e8c420cbf5a803cbb450529f317a Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sat, 17 Jan 2026 01:26:03 +0100 Subject: [PATCH 03/18] Refactor AnimEvent class and update copyright year Updated copyright year and refactored AnimEvent class to use spatialRef for identification. Removed unused model variable and adjusted serialization methods. --- .../com/jme3/cinematic/events/AnimEvent.java | 127 +++++++++++------- 1 file changed, 76 insertions(+), 51 deletions(-) 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 a1a307fac2..d523948e5f 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-2025 jMonkeyEngine + * Copyright (c) 2009-2026 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -45,6 +45,7 @@ import com.jme3.scene.Spatial; import java.io.IOException; +import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; import java.util.logging.Logger; @@ -56,10 +57,17 @@ */ public class AnimEvent extends AbstractCinematicEvent { - public static final Logger logger + private static final Logger logger = Logger.getLogger(AnimEvent.class.getName()); + + private static final String CINEMATIC_REF = "Cinematic:Ref"; - private Spatial model; + private static final AtomicLong spatialId = new AtomicLong(); + + /** + * Reference ID used to find the associated Spatial. + */ + protected String spatialRef; /* * Control that will play the animation */ @@ -76,13 +84,12 @@ public class AnimEvent extends AbstractCinematicEvent { * name of the animation layer on which the action will be played */ private String layerName; - - /** - * Instantiate a non-looping event to play the named action on the default - * layer of the specified AnimComposer. + + /** + * 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 be played + * @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); @@ -99,7 +106,6 @@ public AnimEvent(AnimComposer composer, String actionName) { */ public AnimEvent(AnimComposer composer, String actionName, String layerName) { - this.model = composer.getSpatial(); this.composer = composer; this.actionName = actionName; this.layerName = layerName; @@ -108,6 +114,9 @@ public AnimEvent(AnimComposer composer, String actionName, */ Action eventAction = composer.action(actionName); initialDuration = (float) eventAction.getLength(); + + spatialRef = generateSpatialRef(); + composer.getSpatial().setUserData(CINEMATIC_REF, spatialRef); } /** @@ -117,6 +126,40 @@ protected AnimEvent() { super(); } + /** + * Generate a unique identifier used to tag a Spatial in the scene graph. + * + * @return a unique string identifier + */ + private String generateSpatialRef() { + return "cine" + System.currentTimeMillis() + "_" + (spatialId.incrementAndGet()); + } + + /** + * Recursively search the scene graph for a Spatial whose CINEMATIC_REF + * matches the stored spatialRef. + * + * @param sp the root Spatial to start searching from (not null) + * @return the matching Spatial, or null if not found + */ + private Spatial findModelByRef(Spatial sp) { + String refId = sp.getUserData(CINEMATIC_REF); + if (spatialRef.equals(refId)) { + return sp; + } + + if (sp instanceof Node) { + for (Spatial child : ((Node) sp).getChildren()) { + Spatial model = findModelByRef(child); + if (model != null) { + return model; + } + } + } + + return null; + } + /** * Initialize this event. (for internal use) * @@ -129,22 +172,12 @@ public void initEvent(Application app, Cinematic cinematic) { this.cinematic = cinematic; if (composer == null) { + Spatial model = findModelByRef(cinematic.getScene()); if (model != null) { - if (cinematic.getScene() != null) { - Spatial sceneModel = cinematic.getScene().getChild(model.getName()); - if (sceneModel != null) { - Node parent = sceneModel.getParent(); - parent.detachChild(sceneModel); - sceneModel = model; - parent.attachChild(sceneModel); - } else { - cinematic.getScene().attachChild(model); - } - } composer = model.getControl(AnimComposer.class); - } else { - throw new UnsupportedOperationException("model should not be null"); + throw new UnsupportedOperationException( + "No Spatial found in the scene with Cinematic:Ref=" + spatialRef); } } } @@ -216,30 +249,6 @@ public void onUpdate(float tpf) { // do nothing } - @Override - public void dispose() { - super.dispose(); - cinematic = null; - composer = null; - } - - /** - * De-serialize this event from the specified importer, for example when - * loading from a J3O file. - * - * @param importer (not null) - * @throws IOException from the importer - */ - @Override - public void read(JmeImporter importer) throws IOException { - super.read(importer); - InputCapsule capsule = importer.getCapsule(this); - - model = (Spatial) capsule.readSavable("model", null); - actionName = capsule.readString("actionName", ""); - layerName = capsule.readString("layerName", AnimComposer.DEFAULT_LAYER); - } - /** * Alter the speed of the animation. * @@ -300,6 +309,22 @@ public void setTime(float time) { } } + /** + * De-serialize this event from the specified importer, for example when + * loading from a J3O file. + * + * @param importer (not null) + * @throws IOException from the importer + */ + @Override + public void read(JmeImporter importer) throws IOException { + super.read(importer); + InputCapsule ic = importer.getCapsule(this); + spatialRef = ic.readString("spatialRef", null); + actionName = ic.readString("actionName", null); + layerName = ic.readString("layerName", AnimComposer.DEFAULT_LAYER); + } + /** * Serialize this event to the specified exporter, for example when saving * to a J3O file. @@ -310,9 +335,9 @@ public void setTime(float time) { @Override public void write(JmeExporter exporter) throws IOException { super.write(exporter); - OutputCapsule capsule = exporter.getCapsule(this); - capsule.write(model, "model", null); - capsule.write(actionName, "actionName", ""); - capsule.write(layerName, "layerName", AnimComposer.DEFAULT_LAYER); + OutputCapsule oc = exporter.getCapsule(this); + oc.write(spatialRef, "spatialRef", null); + oc.write(actionName, "actionName", null); + oc.write(layerName, "layerName", AnimComposer.DEFAULT_LAYER); } } From 544b174412d6de38671a4c82ca7bf018bd4d3f83 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sat, 17 Jan 2026 11:10:18 +0100 Subject: [PATCH 04/18] Add dispose method to AnimEvent class Implement dispose method to clean up resources. --- .../src/main/java/com/jme3/cinematic/events/AnimEvent.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 d523948e5f..47881c489b 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 @@ -181,6 +181,13 @@ public void initEvent(Application app, Cinematic cinematic) { } } } + + @Override + public void dispose() { + super.dispose(); + cinematic = null; + composer = null; + } /** * Callback when the event is paused. From 23de7bdcaf80ea143f9d8a4e5e6136f31de9e375 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sat, 17 Jan 2026 12:33:24 +0100 Subject: [PATCH 05/18] Update ArrayList --- jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 169362e6eb..de739756c8 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java @@ -48,7 +48,6 @@ 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; @@ -97,7 +96,7 @@ public class Cinematic extends AbstractCinematicEvent implements AppState { private Node scene; protected TimeLine timeLine = new TimeLine(); private int lastFetchedKeyFrame = -1; - private List cinematicEvents = new ArrayList<>(); + private ArrayList cinematicEvents = new ArrayList<>(); private Map cameras = new HashMap<>(); private CameraNode currentCam; private boolean initialized = false; @@ -241,7 +240,7 @@ public void onPause() { public void write(JmeExporter ex) throws IOException { super.write(ex); OutputCapsule oc = ex.getCapsule(this); - oc.writeSavableArrayList((ArrayList) cinematicEvents, "cinematicEvents", null); + oc.writeSavableArrayList(cinematicEvents, "cinematicEvents", null); oc.writeStringSavableMap(cameras, "cameras", null); oc.write(timeLine, "timeLine", null); } @@ -774,3 +773,4 @@ public void dispose() { } } } + From 2e850c7557a3df4fbf735d7f6d6dd95005fb5009 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sat, 17 Jan 2026 12:52:51 +0100 Subject: [PATCH 06/18] Update Cinematic --- .../main/java/com/jme3/cinematic/Cinematic.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 de739756c8..af98bcff6f 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-2025 jMonkeyEngine + * Copyright (c) 2009-2026 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -48,6 +48,7 @@ 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; @@ -96,7 +97,7 @@ public class Cinematic extends AbstractCinematicEvent implements AppState { private Node scene; protected TimeLine timeLine = new TimeLine(); private int lastFetchedKeyFrame = -1; - private ArrayList cinematicEvents = new ArrayList<>(); + private final List cinematicEvents = new ArrayList<>(); private Map cameras = new HashMap<>(); private CameraNode currentCam; private boolean initialized = false; @@ -240,7 +241,7 @@ public void onPause() { public void write(JmeExporter ex) throws IOException { super.write(ex); OutputCapsule oc = ex.getCapsule(this); - oc.writeSavableArrayList(cinematicEvents, "cinematicEvents", null); + oc.write(cinematicEvents.toArray(new CinematicEvent[cinematicEvents.size()]), "cinematicEvents", null); oc.writeStringSavableMap(cameras, "cameras", null); oc.write(timeLine, "timeLine", null); } @@ -256,7 +257,12 @@ public void write(JmeExporter ex) throws IOException { public void read(JmeImporter im) throws IOException { super.read(im); InputCapsule ic = im.getCapsule(this); - cinematicEvents = ic.readSavableArrayList("cinematicEvents", null); + + Savable[] events = ic.readSavableArray("cinematicEvents", null); + for (Savable c : events) { +// addCinematicEvent(((CinematicEvent) c).getTime(), (CinematicEvent) c) + cinematicEvents.add((CinematicEvent) c); + } cameras = (Map) ic.readStringSavableMap("cameras", null); timeLine = (TimeLine) ic.readSavable("timeLine", null); } @@ -551,7 +557,6 @@ public void postRender() { public void cleanup() { initialized = false; clear(); - clearCameras(); } /** @@ -774,3 +779,4 @@ public void dispose() { } } + From 84dc78060d0285c731b4679ed2ca32374913f6d2 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sat, 17 Jan 2026 17:12:00 +0100 Subject: [PATCH 07/18] fix Cinematic attach/detach/cleanup state --- .../main/java/com/jme3/cinematic/Cinematic.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) 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 af98bcff6f..1cb3f345b5 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java @@ -362,6 +362,12 @@ public boolean isEnabled() { */ @Override public void stateAttached(AppStateManager stateManager) { + for (CameraNode n : cameras.values()) { + if (n.getParent() == null) { + scene.attachChild(n); + logger.log(Level.INFO, "Attached CameraNode to the scene: {0}", n); + } + } } /** @@ -372,6 +378,13 @@ public void stateAttached(AppStateManager stateManager) { @Override public void stateDetached(AppStateManager stateManager) { stop(); + + for (CameraNode n : cameras.values()) { + if (n.getParent() != null) { + scene.detachChild(n); + logger.log(Level.INFO, "Detached CameraNode from the scene: {0}", n); + } + } } /** @@ -556,7 +569,6 @@ public void postRender() { @Override public void cleanup() { initialized = false; - clear(); } /** @@ -780,3 +792,4 @@ public void dispose() { } + From 1bc14f6b3581079d372b14bca1411568e48f9313 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Sat, 17 Jan 2026 17:14:18 +0100 Subject: [PATCH 08/18] remove debug log --- jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java | 5 ----- 1 file changed, 5 deletions(-) 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 1cb3f345b5..c4aa9ba5c3 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java @@ -365,7 +365,6 @@ public void stateAttached(AppStateManager stateManager) { for (CameraNode n : cameras.values()) { if (n.getParent() == null) { scene.attachChild(n); - logger.log(Level.INFO, "Attached CameraNode to the scene: {0}", n); } } } @@ -382,7 +381,6 @@ public void stateDetached(AppStateManager stateManager) { for (CameraNode n : cameras.values()) { if (n.getParent() != null) { scene.detachChild(n); - logger.log(Level.INFO, "Detached CameraNode from the scene: {0}", n); } } } @@ -790,6 +788,3 @@ public void dispose() { } } } - - - From 36881e72fe830c02c9f5da30c78b4b0a70ecc5b6 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 18 Jan 2026 21:16:42 +0100 Subject: [PATCH 09/18] remove unused field --- .../main/java/com/jme3/cinematic/events/AnimEvent.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 47881c489b..b75ec93717 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 @@ -60,7 +60,7 @@ public class AnimEvent extends AbstractCinematicEvent { private static final Logger logger = Logger.getLogger(AnimEvent.class.getName()); - private static final String CINEMATIC_REF = "Cinematic:Ref"; + private static final String CINEMATIC_REF = "Cinematic:Refs"; private static final AtomicLong spatialId = new AtomicLong(); @@ -72,10 +72,6 @@ public class AnimEvent extends AbstractCinematicEvent { * 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 */ @@ -169,7 +165,6 @@ private Spatial findModelByRef(Spatial sp) { @Override public void initEvent(Application app, Cinematic cinematic) { super.initEvent(app, cinematic); - this.cinematic = cinematic; if (composer == null) { Spatial model = findModelByRef(cinematic.getScene()); @@ -185,7 +180,6 @@ public void initEvent(Application app, Cinematic cinematic) { @Override public void dispose() { super.dispose(); - cinematic = null; composer = null; } From 2a3d582c36a6f5ec584cec1c5140ecb1aee9b623 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 18 Jan 2026 21:57:37 +0100 Subject: [PATCH 10/18] relink deserialized AnimEvents from the cinematic appstate --- .../java/com/jme3/cinematic/Cinematic.java | 81 +++++++++++- .../com/jme3/cinematic/events/AnimEvent.java | 116 +++++++----------- 2 files changed, 126 insertions(+), 71 deletions(-) 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 c4aa9ba5c3..cb3f295956 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java @@ -31,11 +31,13 @@ */ 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.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 +45,11 @@ 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 com.jme3.util.clone.Cloner; + import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; @@ -92,6 +97,7 @@ public class Cinematic extends AbstractCinematicEvent implements AppState { private static final Logger logger = Logger.getLogger(Cinematic.class.getName()); + private static final String CINEMATIC_REF = "Cinematic:Refs"; private Application app; private Node scene; @@ -241,7 +247,29 @@ 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); + CinematicEvent[] events = new CinematicEvent[cinematicEvents.size()]; + for (int i = 0; i < cinematicEvents.size(); i++) { + CinematicEvent ce = cinematicEvents.get(i); + if (ce instanceof AnimEvent) { + AnimEvent animEvent = (AnimEvent) ce; + + // 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); + + // HACK: create a clone of the event without the composer + // this is used to make this appstate deserializable without + // breaking the scene graph + Cloner cloner = new Cloner(); + animEvent = (AnimEvent) cloner.clone(animEvent); + animEvent.setComposer(null); + ce = animEvent; + + } + events[i] = ce; + } + oc.write(events, "cinematicEvents", null); oc.writeStringSavableMap(cameras, "cameras", null); oc.write(timeLine, "timeLine", null); } @@ -260,7 +288,6 @@ public void read(JmeImporter im) throws IOException { Savable[] events = ic.readSavableArray("cinematicEvents", null); for (Savable c : events) { -// addCinematicEvent(((CinematicEvent) c).getTime(), (CinematicEvent) c) cinematicEvents.add((CinematicEvent) c); } cameras = (Map) ic.readStringSavableMap("cameras", null); @@ -294,9 +321,23 @@ public void setSpeed(float speed) { public void initialize(AppStateManager stateManager, Application app) { this.app = app; initEvent(app, this); + for (CinematicEvent cinematicEvent : cinematicEvents) { + 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) { + composer = sp.getControl(AnimComposer.class); + animEvent.setComposer(composer); + } + } + } cinematicEvent.initEvent(app, this); } + if (!cameras.isEmpty()) { for (CameraNode n : cameras.values()) { n.setCamera(app.getCamera()); @@ -305,6 +346,42 @@ public void initialize(AppStateManager stateManager, Application app) { initialized = true; } + @SuppressWarnings("unchecked") + private Spatial findModelByRef(Spatial sp, String spatialRef) { + Object refIdsObj = sp.getUserData(CINEMATIC_REF); + if ((refIdsObj instanceof List)) { + List refIds = (List) refIdsObj; + for (String refId : refIds) { + if (spatialRef.equals(refId)) { + return sp; + } + } + } + if (sp instanceof Node) { + for (Spatial child : ((Node) sp).getChildren()) { + Spatial model = findModelByRef(child, spatialRef); + if (model != null) { + return model; + } + } + } + return null; + } + + @SuppressWarnings("unchecked") + private void setModelRefId(Spatial sp, String spatialRef) { + Object refIdsObj = sp.getUserData(CINEMATIC_REF); + List refIds; + if (refIdsObj instanceof List) { + refIds = (List) refIdsObj; + if (refIds.contains(spatialRef)) return; + } else { + refIds = new ArrayList<>(); + sp.setUserData(CINEMATIC_REF, refIds); + } + refIds.add(spatialRef); + } + /** * used internally * 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 b75ec93717..6428f322c1 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 @@ -41,8 +41,8 @@ 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.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; import java.io.IOException; import java.util.concurrent.atomic.AtomicLong; @@ -55,19 +55,18 @@ * * Inspired by Nehon's {@link AnimationEvent}. */ -public class AnimEvent extends AbstractCinematicEvent { +public class AnimEvent extends AbstractCinematicEvent implements JmeCloneable { private static final Logger logger = Logger.getLogger(AnimEvent.class.getName()); - - private static final String CINEMATIC_REF = "Cinematic:Refs"; - private static final AtomicLong spatialId = new AtomicLong(); + + private static final AtomicLong refCounter = new AtomicLong(); /** - * Reference ID used to find the associated Spatial. + * Unique-ish id that identify this anim event */ - protected String spatialRef; + protected String animRef; /* * Control that will play the animation */ @@ -102,6 +101,7 @@ public AnimEvent(AnimComposer composer, String actionName) { */ public AnimEvent(AnimComposer composer, String actionName, String layerName) { + this(); this.composer = composer; this.actionName = actionName; this.layerName = layerName; @@ -110,9 +110,6 @@ public AnimEvent(AnimComposer composer, String actionName, */ Action eventAction = composer.action(actionName); initialDuration = (float) eventAction.getLength(); - - spatialRef = generateSpatialRef(); - composer.getSpatial().setUserData(CINEMATIC_REF, spatialRef); } /** @@ -120,40 +117,7 @@ public AnimEvent(AnimComposer composer, String actionName, */ protected AnimEvent() { super(); - } - - /** - * Generate a unique identifier used to tag a Spatial in the scene graph. - * - * @return a unique string identifier - */ - private String generateSpatialRef() { - return "cine" + System.currentTimeMillis() + "_" + (spatialId.incrementAndGet()); - } - - /** - * Recursively search the scene graph for a Spatial whose CINEMATIC_REF - * matches the stored spatialRef. - * - * @param sp the root Spatial to start searching from (not null) - * @return the matching Spatial, or null if not found - */ - private Spatial findModelByRef(Spatial sp) { - String refId = sp.getUserData(CINEMATIC_REF); - if (spatialRef.equals(refId)) { - return sp; - } - - if (sp instanceof Node) { - for (Spatial child : ((Node) sp).getChildren()) { - Spatial model = findModelByRef(child); - if (model != null) { - return model; - } - } - } - - return null; + animRef = "animEvent-" + System.currentTimeMillis() + "_" + refCounter.incrementAndGet(); } /** @@ -165,24 +129,9 @@ private Spatial findModelByRef(Spatial sp) { @Override public void initEvent(Application app, Cinematic cinematic) { super.initEvent(app, cinematic); - - if (composer == null) { - Spatial model = findModelByRef(cinematic.getScene()); - if (model != null) { - composer = model.getControl(AnimComposer.class); - } else { - throw new UnsupportedOperationException( - "No Spatial found in the scene with Cinematic:Ref=" + spatialRef); - } - } - } - - @Override - public void dispose() { - super.dispose(); - composer = null; } + /** * Callback when the event is paused. */ @@ -320,10 +269,11 @@ public void setTime(float time) { @Override public void read(JmeImporter importer) throws IOException { super.read(importer); - InputCapsule ic = importer.getCapsule(this); - spatialRef = ic.readString("spatialRef", null); - actionName = ic.readString("actionName", null); - layerName = ic.readString("layerName", AnimComposer.DEFAULT_LAYER); + InputCapsule capsule = importer.getCapsule(this); + actionName = capsule.readString("actionName", ""); + composer = (AnimComposer) capsule.readSavable("composer", null); + layerName = capsule.readString("layerName", AnimComposer.DEFAULT_LAYER); + animRef = capsule.readString("animRef", null); } /** @@ -336,9 +286,37 @@ public void read(JmeImporter importer) throws IOException { @Override public void write(JmeExporter exporter) throws IOException { super.write(exporter); - OutputCapsule oc = exporter.getCapsule(this); - oc.write(spatialRef, "spatialRef", null); - oc.write(actionName, "actionName", null); - oc.write(layerName, "layerName", AnimComposer.DEFAULT_LAYER); + OutputCapsule capsule = exporter.getCapsule(this); + capsule.write(actionName, "actionName", ""); + 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; } + + @Override + public Object jmeClone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException("Can't clone AnimEvent", e); + } + } + + @Override + public void cloneFields(Cloner cloner, Object original) { + + } + } From 7d9880d29b58ab0a66902fb4b068e3ec7d60f8f2 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 19 Jan 2026 17:55:27 +0100 Subject: [PATCH 11/18] throw if spatial is not found --- jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 cb3f295956..818bcdc02a 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java @@ -329,10 +329,12 @@ public void initialize(AppStateManager stateManager, Application app) { if (composer == null) { String ref = animEvent.getAnimRef(); Spatial sp = findModelByRef(scene, ref); - if (sp != null) { - composer = sp.getControl(AnimComposer.class); - animEvent.setComposer(composer); + if (sp == null) { + throw new IllegalStateException( + "Cannot find model with ref id " + ref + " for AnimEvent"); } + composer = sp.getControl(AnimComposer.class); + animEvent.setComposer(composer); } } cinematicEvent.initEvent(app, this); From 3a784d97c5922c14cabe46cc9c6fc6ff7f31dafc Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 19 Jan 2026 17:55:40 +0100 Subject: [PATCH 12/18] add tests --- .../animation/TestAnimEventSavable.java | 147 ++++++++++++++ .../animation/TestCinematicSavable.java | 188 ++++++++++++++++++ 2 files changed, 335 insertions(+) create mode 100644 jme3-examples/src/main/java/jme3test/animation/TestAnimEventSavable.java create mode 100644 jme3-examples/src/main/java/jme3test/animation/TestCinematicSavable.java 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..66170bbb49 --- /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); + // cinematic.initialize(stateManager, this); + // stateManager.attach(cinematic); + + 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); + stateManager.detach(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) { + if (cinematic.getPlayState() == PlayState.Playing) { + cinematic.pause(); + } else { + System.out.println("Play"); + cinematic.play(); + } + } + + } + }; + inputManager.addListener(acl, "togglePlay"); + } +} \ No newline at end of file From fe2ab037587a60d75b8e35f77810f334b4b16477 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 19 Jan 2026 18:17:53 +0100 Subject: [PATCH 13/18] add cinematic abstraction --- .../java/com/jme3/cinematic/Cinematic.java | 15 +- .../com/jme3/cinematic/CinematicHandler.java | 130 ++++++++++++++++++ .../events/AbstractCinematicEvent.java | 4 +- .../com/jme3/cinematic/events/AnimEvent.java | 7 +- .../jme3/cinematic/events/AnimationEvent.java | 7 +- .../jme3/cinematic/events/CameraEvent.java | 12 +- .../jme3/cinematic/events/CinematicEvent.java | 4 +- .../jme3/cinematic/events/MotionEvent.java | 4 +- .../com/jme3/cinematic/events/SoundEvent.java | 4 +- 9 files changed, 168 insertions(+), 19 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java 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 818bcdc02a..c596df6745 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java @@ -94,7 +94,7 @@ * * @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 = "Cinematic:Refs"; @@ -544,6 +544,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) { @@ -565,6 +566,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); @@ -578,6 +580,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); @@ -597,6 +600,7 @@ 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) { KeyFrame keyFrame = timeLine.getKeyFrameAtTime(timeStamp); return removeCinematicEvent(keyFrame, cinematicEvent); @@ -610,6 +614,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); @@ -677,6 +682,7 @@ public void fitDuration() { * @param cam the scene camera. * @return the created CameraNode. */ + @Override public CameraNode bindCamera(String cameraName, Camera cam) { if (cameras.containsKey(cameraName)) { throw new IllegalArgumentException("Camera " + cameraName + " is already bound to this cinematic"); @@ -696,6 +702,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); } @@ -718,6 +725,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); @@ -734,6 +742,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)); } @@ -757,6 +766,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); @@ -774,6 +784,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); @@ -790,6 +801,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); @@ -849,6 +861,7 @@ public void clear() { * This method removes all previously bound CameraNodes and clears the * internal camera map, effectively detaching all cameras from the scene. */ + @Override public void clearCameras() { for (CameraNode cameraNode : cameras.values()) { scene.detachChild(cameraNode); 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..d005bc8058 --- /dev/null +++ b/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java @@ -0,0 +1,130 @@ +package com.jme3.cinematic; + +import com.jme3.cinematic.events.CinematicEvent; +import com.jme3.renderer.Camera; +import com.jme3.scene.CameraNode; + +public interface CinematicHandler { + + /** + * 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); + + /** + * Binds a camera to this Cinematic, tagged by a unique name. This method + * creates and returns a CameraNode for the cam and attaches 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 + * playback. + * + * @param cameraName the unique tag the camera should have + * @param cam the scene camera. + * @return the created CameraNode. + */ + CameraNode bindCamera(String cameraName, Camera cam); + + /** + * 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); + + /** + * 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. + */ + void clearCameras(); + +} \ 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..5bad3e09df 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 @@ -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.PlayState; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; @@ -317,7 +317,7 @@ public void read(JmeImporter im) throws IOException { * @param cinematic ignored */ @Override - public void initEvent(Application app, Cinematic cinematic) { + public void initEvent(Application app, CinematicHandler 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 6428f322c1..3a69eabd42 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 @@ -36,6 +36,7 @@ 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; @@ -127,10 +128,14 @@ protected AnimEvent() { * @param cinematic the Cinematic that contains this event */ @Override - public void initEvent(Application app, Cinematic cinematic) { + public void initEvent(Application app, CinematicHandler cinematic) { super.initEvent(app, cinematic); } + @Deprecated + public void initEvent(Application app, Cinematic cinematic) { + initEvent(app, (CinematicHandler) cinematic); + } /** * Callback when the event is paused. 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..03fa7b301d 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 @@ -34,6 +34,7 @@ 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 +288,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(Application app, CinematicHandler handler) { + super.initEvent(app, 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..f7d4fe7dd9 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 @@ -32,7 +32,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; @@ -55,7 +55,7 @@ public class CameraEvent extends AbstractCinematicEvent { * The `Cinematic` instance to which this event belongs and on which the * camera will be set. */ - private Cinematic cinematic; + private CinematicHandler cinematic; /** * For serialization only. Do not use. @@ -69,13 +69,13 @@ 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) { + public CameraEvent(CinematicHandler cinematic, String cameraName) { this.cinematic = cinematic; this.cameraName = cameraName; } @Override - public void initEvent(Application app, Cinematic cinematic) { + public void initEvent(Application app, CinematicHandler cinematic) { super.initEvent(app, cinematic); this.cinematic = cinematic; } @@ -116,7 +116,7 @@ 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; } @@ -124,7 +124,7 @@ public Cinematic getCinematic() { * Sets the `Cinematic` instance for this event. * @param cinematic The `Cinematic` instance to set (cannot be null). */ - public void setCinematic(Cinematic cinematic) { + public void setCinematic(CinematicHandler cinematic) { this.cinematic = cinematic; } 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..6e77861e72 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 @@ -33,7 +33,7 @@ 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.Savable; @@ -135,7 +135,7 @@ public interface CinematicEvent extends Savable { * @param app the application * @param cinematic the cinematic */ - public void initEvent(Application app, Cinematic cinematic); + public void initEvent(Application app, CinematicHandler cinematic); /** * Fast-forwards to the given time, where time=0 is the start of the event. 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..2c3675d4d8 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,7 +197,7 @@ public void internalUpdate(float tpf) { } @Override - public void initEvent(Application app, Cinematic cinematic) { + public void initEvent(Application app, CinematicHandler cinematic) { super.initEvent(app, 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..e99fa681cf 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 @@ -36,7 +36,7 @@ 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,7 +152,7 @@ public SoundEvent() { } @Override - public void initEvent(Application app, Cinematic cinematic) { + public void initEvent(Application app, CinematicHandler cinematic) { super.initEvent(app, cinematic); audioNode = new AudioNode(app.getAssetManager(), path, stream ? AudioData.DataType.Stream : AudioData.DataType.Buffer); audioNode.setPositional(false); From a5970970a4bf8445d0576570ab63a4eacfba81c9 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 19 Jan 2026 18:23:06 +0100 Subject: [PATCH 14/18] make cinematic handler savable --- .../src/main/java/com/jme3/cinematic/CinematicHandler.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java b/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java index d005bc8058..7e98586807 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java @@ -1,10 +1,11 @@ package com.jme3.cinematic; import com.jme3.cinematic.events.CinematicEvent; +import com.jme3.export.Savable; import com.jme3.renderer.Camera; import com.jme3.scene.CameraNode; -public interface CinematicHandler { +public interface CinematicHandler extends Savable { /** * Adds a cinematic event to this cinematic at the given timestamp. This From de30e2e2c08c474e9f124fb08698f45f713ed5e7 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 19 Jan 2026 18:27:47 +0100 Subject: [PATCH 15/18] move deprecated fallback into CinematicEvent --- .../src/main/java/com/jme3/cinematic/events/AnimEvent.java | 5 ----- .../main/java/com/jme3/cinematic/events/CinematicEvent.java | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) 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 3a69eabd42..8158b111e3 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 @@ -132,11 +132,6 @@ public void initEvent(Application app, CinematicHandler cinematic) { super.initEvent(app, cinematic); } - @Deprecated - public void initEvent(Application app, Cinematic cinematic) { - initEvent(app, (CinematicHandler) cinematic); - } - /** * Callback when the event is paused. */ 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 6e77861e72..039406e4e9 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 @@ -33,6 +33,7 @@ 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.Savable; @@ -137,6 +138,11 @@ public interface CinematicEvent extends Savable { */ public void initEvent(Application app, CinematicHandler cinematic); + @Deprecated + public default void initEvent(Application app, Cinematic cinematic) { + initEvent(app, (CinematicHandler) cinematic); + } + /** * Fast-forwards to the given time, where time=0 is the start of the event. * From 1c4850981aea87c5659e97a96fd71ecc6b559313 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 19 Jan 2026 18:39:25 +0100 Subject: [PATCH 16/18] make CinematicHandler extend CinematicEvent for backward compatibility --- .../com/jme3/cinematic/CinematicHandler.java | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java b/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java index 7e98586807..7b80329e19 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java @@ -1,11 +1,15 @@ package com.jme3.cinematic; +import com.jme3.app.Application; import com.jme3.cinematic.events.CinematicEvent; -import com.jme3.export.Savable; +import com.jme3.cinematic.events.CinematicEventListener; import com.jme3.renderer.Camera; import com.jme3.scene.CameraNode; -public interface CinematicHandler extends Savable { +/** + * A interface that defines an object that can compose and coordinate cinematic events. + */ +public interface CinematicHandler extends CinematicEvent { /** * Adds a cinematic event to this cinematic at the given timestamp. This @@ -128,4 +132,32 @@ public interface CinematicHandler extends Savable { */ void clearCameras(); + /** + * DO NOT implement this. This is a left-over from the previous problematic abstraction of the Cinematic + * class. Kept just for backward compatibility. + * + * @param app + * @param cinematic + */ + @Deprecated + public default void initEvent(Application app, CinematicHandler cinematic) { + + } + + /** + * Adds a CinematicEventListener to this handler. + * + * @param listener + * CinematicEventListener + */ + void addListener(CinematicEventListener listener); + + /** + * Removes a CinematicEventListener from this handler. + * + * @param listener + * CinematicEventListener + */ + void removeListener(CinematicEventListener listener); + } \ No newline at end of file From a6a5df18788c6c1c056146fc9afff1d0bf5ccbb7 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 20 Jan 2026 21:45:30 +0100 Subject: [PATCH 17/18] improve abstraction --- .../java/com/jme3/cinematic/Cinematic.java | 172 +++++++++++------- .../com/jme3/cinematic/CinematicBase.java | 104 +++++++++++ .../com/jme3/cinematic/CinematicHandler.java | 55 +----- .../events/AbstractCinematicEvent.java | 22 ++- .../com/jme3/cinematic/events/AnimEvent.java | 26 +-- .../jme3/cinematic/events/AnimationEvent.java | 5 +- .../jme3/cinematic/events/CameraEvent.java | 23 +-- .../jme3/cinematic/events/CinematicEvent.java | 108 +---------- .../jme3/cinematic/events/MotionEvent.java | 4 +- .../com/jme3/cinematic/events/SoundEvent.java | 18 +- .../animation/TestCinematicSavable.java | 6 +- 11 files changed, 268 insertions(+), 275 deletions(-) create mode 100644 jme3-core/src/main/java/com/jme3/cinematic/CinematicBase.java 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 c596df6745..bf41ac1a6a 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java @@ -36,6 +36,7 @@ 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; @@ -48,7 +49,6 @@ import com.jme3.scene.Spatial; import com.jme3.scene.control.CameraControl; import com.jme3.scene.control.CameraControl.ControlDirection; -import com.jme3.util.clone.Cloner; import java.io.IOException; import java.util.ArrayList; @@ -97,7 +97,8 @@ public class Cinematic extends AbstractCinematicEvent implements AppState, CinematicHandler { private static final Logger logger = Logger.getLogger(Cinematic.class.getName()); - private static final String CINEMATIC_REF = "Cinematic:Refs"; + private static final String CINEMATIC_REF_PREFIX = "Cinematic:Refs:"; + private final String CINEMATIC_REF_VALUE = "jme3:Cinematic"; private Application app; private Node scene; @@ -237,6 +238,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 * @@ -247,31 +283,26 @@ public void onPause() { public void write(JmeExporter ex) throws IOException { super.write(ex); OutputCapsule oc = ex.getCapsule(this); - CinematicEvent[] events = new CinematicEvent[cinematicEvents.size()]; - for (int i = 0; i < cinematicEvents.size(); i++) { - CinematicEvent ce = cinematicEvents.get(i); - if (ce instanceof AnimEvent) { - AnimEvent animEvent = (AnimEvent) ce; - - // 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); - - // HACK: create a clone of the event without the composer - // this is used to make this appstate deserializable without - // breaking the scene graph - Cloner cloner = new Cloner(); - animEvent = (AnimEvent) cloner.clone(animEvent); - animEvent.setComposer(null); - ce = animEvent; + 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); } - events[i] = ce; } - oc.write(events, "cinematicEvents", null); - oc.writeStringSavableMap(cameras, "cameras", null); - oc.write(timeLine, "timeLine", null); } /** @@ -288,6 +319,7 @@ public void read(JmeImporter im) throws IOException { Savable[] events = ic.readSavableArray("cinematicEvents", null); for (Savable c : events) { + relinkCinematic((CinematicEvent) c); cinematicEvents.add((CinematicEvent) c); } cameras = (Map) ic.readStringSavableMap("cameras", null); @@ -322,21 +354,10 @@ public void initialize(AppStateManager stateManager, Application app) { this.app = app; initEvent(app, this); + System.out.println("Initializing"); for (CinematicEvent cinematicEvent : cinematicEvents) { - 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); - } - } + relinkAnimComposer(cinematicEvent); + relinkCinematic(cinematicEvent); cinematicEvent.initEvent(app, this); } @@ -348,17 +369,36 @@ public void initialize(AppStateManager stateManager, Application app) { initialized = true; } - @SuppressWarnings("unchecked") - private Spatial findModelByRef(Spatial sp, String spatialRef) { - Object refIdsObj = sp.getUserData(CINEMATIC_REF); - if ((refIdsObj instanceof List)) { - List refIds = (List) refIdsObj; - for (String refId : refIds) { - if (spatialRef.equals(refId)) { - return sp; + 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); @@ -370,18 +410,9 @@ private Spatial findModelByRef(Spatial sp, String spatialRef) { return null; } - @SuppressWarnings("unchecked") private void setModelRefId(Spatial sp, String spatialRef) { - Object refIdsObj = sp.getUserData(CINEMATIC_REF); - List refIds; - if (refIdsObj instanceof List) { - refIds = (List) refIdsObj; - if (refIds.contains(spatialRef)) return; - } else { - refIds = new ArrayList<>(); - sp.setUserData(CINEMATIC_REF, refIds); - } - refIds.add(spatialRef); + String key = CINEMATIC_REF_PREFIX + spatialRef; + sp.setUserData(key, CINEMATIC_REF_VALUE); } /** @@ -472,6 +503,7 @@ public void stateDetached(AppStateManager stateManager) { @Override public void update(float tpf) { if (isInitialized() && playState == PlayState.Playing) { + System.out.println("Updating cinematic at time " + cinematic); internalUpdate(tpf); } } @@ -672,17 +704,16 @@ public void fitDuration() { } /** - * Binds a camera to this Cinematic, tagged by a unique name. This method - * creates and returns a CameraNode for the cam and attaches 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 - * playback. + * Binds a camera to this Cinematic, tagged by a unique name. This method creates and returns a CameraNode + * for the cam and attaches 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 playback. * - * @param cameraName the unique tag the camera should have - * @param cam the scene camera. + * @param cameraName + * the unique tag the camera should have + * @param cam + * the scene camera. * @return the created CameraNode. */ - @Override public CameraNode bindCamera(String cameraName, Camera cam) { if (cameras.containsKey(cameraName)) { throw new IllegalArgumentException("Camera " + cameraName + " is already bound to this cinematic"); @@ -857,11 +888,9 @@ 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. + * 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. */ - @Override public void clearCameras() { for (CameraNode cameraNode : cameras.values()) { scene.detachChild(cameraNode); @@ -879,4 +908,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 index 7b80329e19..d93fe8626c 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/CinematicHandler.java @@ -1,15 +1,14 @@ package com.jme3.cinematic; -import com.jme3.app.Application; +import com.jme3.asset.AssetManager; import com.jme3.cinematic.events.CinematicEvent; -import com.jme3.cinematic.events.CinematicEventListener; -import com.jme3.renderer.Camera; +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 CinematicEvent { +public interface CinematicHandler extends CinematicBase, Savable { /** * Adds a cinematic event to this cinematic at the given timestamp. This @@ -59,19 +58,6 @@ public interface CinematicHandler extends CinematicEvent { */ boolean removeCinematicEvent(KeyFrame keyFrame, CinematicEvent cinematicEvent); - /** - * Binds a camera to this Cinematic, tagged by a unique name. This method - * creates and returns a CameraNode for the cam and attaches 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 - * playback. - * - * @param cameraName the unique tag the camera should have - * @param cam the scene camera. - * @return the created CameraNode. - */ - CameraNode bindCamera(String cameraName, Camera cam); - /** * returns a cameraNode given its name * @@ -126,38 +112,9 @@ public interface CinematicHandler extends CinematicEvent { void removeEventData(String type, Object key); /** - * 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. - */ - void clearCameras(); - - /** - * DO NOT implement this. This is a left-over from the previous problematic abstraction of the Cinematic - * class. Kept just for backward compatibility. + * Returns the AssetManager associated with this CinematicHandler. * - * @param app - * @param cinematic + * @return The AssetManager instance. */ - @Deprecated - public default void initEvent(Application app, CinematicHandler cinematic) { - - } - - /** - * Adds a CinematicEventListener to this handler. - * - * @param listener - * CinematicEventListener - */ - void addListener(CinematicEventListener listener); - - /** - * Removes a CinematicEventListener from this handler. - * - * @param listener - * CinematicEventListener - */ - void removeListener(CinematicEventListener listener); - + 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 5bad3e09df..1a16f4fd1a 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,13 +33,15 @@ import com.jme3.animation.AnimationUtils; import com.jme3.animation.LoopMode; -import com.jme3.app.Application; 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 com.jme3.util.clone.Cloner; +import com.jme3.util.clone.JmeCloneable; + import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -60,6 +62,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 +297,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 +312,10 @@ 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); + CinematicHandler cinematic = (CinematicHandler) ic.readSavable("cinematic", null); + if (cinematic != null) { + this.cinematic = cinematic; + } } /** @@ -317,7 +325,8 @@ public void read(JmeImporter im) throws IOException { * @param cinematic ignored */ @Override - public void initEvent(Application app, CinematicHandler cinematic) { + public void initEvent(CinematicHandler cinematic) { + this.cinematic = cinematic; } /** @@ -370,4 +379,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 8158b111e3..e92955cc82 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 @@ -34,17 +34,12 @@ 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 com.jme3.util.clone.Cloner; -import com.jme3.util.clone.JmeCloneable; - import java.io.IOException; import java.util.concurrent.atomic.AtomicLong; import java.util.logging.Level; @@ -56,7 +51,7 @@ * * Inspired by Nehon's {@link AnimationEvent}. */ -public class AnimEvent extends AbstractCinematicEvent implements JmeCloneable { +public class AnimEvent extends AbstractCinematicEvent { private static final Logger logger = Logger.getLogger(AnimEvent.class.getName()); @@ -128,8 +123,8 @@ protected AnimEvent() { * @param cinematic the Cinematic that contains this event */ @Override - public void initEvent(Application app, CinematicHandler cinematic) { - super.initEvent(app, cinematic); + public void initEvent(CinematicHandler cinematic) { + super.initEvent(cinematic); } /** @@ -304,19 +299,4 @@ public void setComposer(AnimComposer composer) { public String getAnimRef() { return animRef; } - - @Override - public Object jmeClone() { - try { - return super.clone(); - } catch (CloneNotSupportedException e) { - throw new RuntimeException("Can't clone AnimEvent", e); - } - } - - @Override - public void cloneFields(Cloner cloner, Object original) { - - } - } 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 03fa7b301d..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,7 +32,6 @@ 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; @@ -288,8 +287,8 @@ public AnimationEvent(Spatial model, String animationName, float initialDuration @Override @SuppressWarnings("unchecked") - public void initEvent(Application app, CinematicHandler handler) { - super.initEvent(app, handler); + public void initEvent(CinematicHandler handler) { + super.initEvent(handler); this.cinematic = (Cinematic) handler; if (channel == null) { Object s = cinematic.getEventData(MODEL_CHANNELS, model); 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 f7d4fe7dd9..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,7 +31,6 @@ */ package com.jme3.cinematic.events; -import com.jme3.app.Application; import com.jme3.cinematic.CinematicHandler; import com.jme3.export.InputCapsule; import com.jme3.export.JmeExporter; @@ -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 CinematicHandler cinematic; /** * For serialization only. Do not use. @@ -70,14 +64,13 @@ public CameraEvent() { * @param cameraName The name of the camera to be activated by this event (cannot be null or empty). */ public CameraEvent(CinematicHandler cinematic, String cameraName) { - this.cinematic = cinematic; this.cameraName = cameraName; + setCinematic(cinematic); } @Override - public void initEvent(Application app, CinematicHandler 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 @@ -120,14 +113,6 @@ 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(CinematicHandler 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 039406e4e9..fe22e5a020 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,99 +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.CinematicHandler; -import com.jme3.cinematic.PlayState; +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 @@ -133,29 +51,17 @@ public interface CinematicEvent extends Savable { /** * initialize this event - * @param app the application - * @param cinematic the cinematic + * + * @param cinematic + * the cinematic */ - public void initEvent(Application app, CinematicHandler cinematic); + public void initEvent(CinematicHandler cinematic); @Deprecated public default void initEvent(Application app, Cinematic cinematic) { - initEvent(app, (CinematicHandler) cinematic); + initEvent((CinematicHandler) 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(); - /** * method called when an event is removed from a cinematic * this method should remove any reference to any external objects. 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 2c3675d4d8..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 @@ -197,8 +197,8 @@ public void internalUpdate(float tpf) { } @Override - public void initEvent(Application app, CinematicHandler 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 e99fa681cf..a177a383fc 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,7 +32,6 @@ 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; @@ -152,10 +151,13 @@ public SoundEvent() { } @Override - public void initEvent(Application app, CinematicHandler 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); + System.out.println("SoundEvent: loaded sound from " + path + this + audioNode + " for cinematic " + + getCinematic()); setLoopMode(loopMode); } @@ -172,7 +174,10 @@ public void setTime(float time) { @Override public void onPlay() { + System.out.println("SoundEvent: play sound from " + path + this + audioNode + " for cinematic " + + getCinematic()); audioNode.play(); + } @Override @@ -217,6 +222,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 +231,9 @@ public void read(JmeImporter im) throws IOException { InputCapsule ic = im.getCapsule(this); path = ic.readString("path", ""); stream = ic.readBoolean("stream", false); + AudioNode audioNode = (AudioNode) ic.readSavable("audioNode", null); + if (audioNode != null) { + this.audioNode = audioNode; + } } } diff --git a/jme3-examples/src/main/java/jme3test/animation/TestCinematicSavable.java b/jme3-examples/src/main/java/jme3test/animation/TestCinematicSavable.java index 66170bbb49..ee51fc1b6e 100644 --- a/jme3-examples/src/main/java/jme3test/animation/TestCinematicSavable.java +++ b/jme3-examples/src/main/java/jme3test/animation/TestCinematicSavable.java @@ -51,8 +51,7 @@ public void simpleInitApp() { viewPort.setBackgroundColor(ColorRGBA.DarkGray); cinematic = new Cinematic(rootNode, 10); - // cinematic.initialize(stateManager, this); - // stateManager.attach(cinematic); + setupLightsAndFilters(); setupModel(); @@ -95,7 +94,6 @@ public void onStop(CinematicEvent cinematic) { }); Cinematic copy = BinaryExporter.saveAndLoad(assetManager, cinematic); - stateManager.detach(cinematic); cinematic = copy; cinematic.setScene(rootNode); @@ -173,6 +171,8 @@ private void initInputs() { @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 { From b9b4d81c34a4fc90af55dc36f534be6327f18066 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 20 Jan 2026 22:11:02 +0100 Subject: [PATCH 18/18] cleanup --- .../java/com/jme3/cinematic/Cinematic.java | 85 ++++++++----------- .../events/AbstractCinematicEvent.java | 8 +- .../com/jme3/cinematic/events/AnimEvent.java | 38 ++++----- .../jme3/cinematic/events/CinematicEvent.java | 9 +- .../com/jme3/cinematic/events/SoundEvent.java | 10 +-- 5 files changed, 61 insertions(+), 89 deletions(-) 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 bf41ac1a6a..b338ae3c6d 100644 --- a/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java +++ b/jme3-core/src/main/java/com/jme3/cinematic/Cinematic.java @@ -59,38 +59,36 @@ import java.util.logging.Logger; /** - * An AppState for composing and playing cutscenes in a game. + * An appstate for composing and playing cutscenes in a game. The cinematic + * schedules CinematicEvents over a timeline. Once the Cinematic created it has + * to be attached to the stateManager. * - *

A cinematic schedules and plays {@link CinematicEvent}s over a timeline. - * Once a Cinematic is created, you must attach it to the `AppStateManager` to - * run it. You can add various `CinematicEvent`s, see the `com.jme3.cinematic.events` - * package for built-in event types. + * You can add various CinematicEvents to a Cinematic, see package + * com.jme3.cinematic.events * - *

Events can be added in two main ways: - *

    - *
  • {@link Cinematic#addCinematicEvent(float, CinematicEvent)} adds an event - * at a specific time from the cinematic's start.
  • - *
  • {@link Cinematic#enqueueCinematicEvent(CinematicEvent)} adds events - * one after another, with each starting at the end of the previous one.
  • - *
+ * Two main methods can be used to add an event : * - *

Playback can be controlled with methods like: - *

    - *
  • {@link Cinematic#play()}
  • - *
  • {@link Cinematic#pause()}
  • - *
  • {@link Cinematic#stop()}
  • - *
+ * @see Cinematic#addCinematicEvent(float, + * com.jme3.cinematic.events.CinematicEvent) , that adds an event at the given + * time form the cinematic start. * - *

Since `Cinematic` itself extends `CinematicEvent`, you can nest cinematics - * within each other. Nested cinematics should not be attached to the `AppStateManager`. + * @see + * Cinematic#enqueueCinematicEvent(com.jme3.cinematic.events.CinematicEvent) + * that enqueue events one after the other according to their initialDuration * - *

This class also handles multiple camera points of view by creating and - * activating camera nodes on a schedule. - *

    - *
  • {@link Cinematic#bindCamera(java.lang.String, com.jme3.renderer.Camera)}
  • - *
  • {@link Cinematic#activateCamera(float, java.lang.String)}
  • - *
  • {@link Cinematic#setActiveCamera(java.lang.String)}
  • - *
+ * A Cinematic has convenient methods to manage playback: + * @see Cinematic#play() + * @see Cinematic#pause() + * @see Cinematic#stop() + * + * A Cinematic is itself a CinematicEvent, meaning you can embed several + * cinematics. Embedded cinematics must not be added to the stateManager though. + * + * Cinematic can handle several points of view by creating camera nodes + * 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 */ @@ -295,8 +293,8 @@ public void write(JmeExporter ex) throws IOException { oc.write(cinematicEvents.toArray(new CinematicEvent[cinematicEvents.size()]), "cinematicEvents", null); - oc.writeStringSavableMap(cameras, "cameras", null); - oc.write(timeLine, "timeLine", null); + oc.writeStringSavableMap(cameras, "cameras", null); + oc.write(timeLine, "timeLine", null); } finally { // unhack for (CinematicEvent event : cinematicEvents) { @@ -353,8 +351,6 @@ public void setSpeed(float speed) { public void initialize(AppStateManager stateManager, Application app) { this.app = app; initEvent(app, this); - - System.out.println("Initializing"); for (CinematicEvent cinematicEvent : cinematicEvents) { relinkAnimComposer(cinematicEvent); relinkCinematic(cinematicEvent); @@ -503,7 +499,6 @@ public void stateDetached(AppStateManager stateManager) { @Override public void update(float tpf) { if (isInitialized() && playState == PlayState.Playing) { - System.out.println("Updating cinematic at time " + cinematic); internalUpdate(tpf); } } @@ -704,14 +699,14 @@ public void fitDuration() { } /** - * Binds a camera to this Cinematic, tagged by a unique name. This method creates and returns a CameraNode - * for the cam and attaches 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 playback. + * Binds a camera to this Cinematic, tagged by a unique name. This method + * creates and returns a CameraNode for the cam and attaches 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 + * playback. * - * @param cameraName - * the unique tag the camera should have - * @param cam - * the scene camera. + * @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) { @@ -739,9 +734,9 @@ public CameraNode getCamera(String cameraName) { } /** - * Enables or disables the camera control of the cameraNode of the current cam. + * enable/disable the camera control of the cameraNode of the current cam * - * @param enabled `true` to enable, `false` to disable. + * @param enabled */ private void setEnableCurrentCam(boolean enabled) { if (currentCam != null) { @@ -866,14 +861,6 @@ public Node getScene() { return scene; } - /** - * Gets the application instance associated with this cinematic. - * - * @return The application. - */ - public Application getApplication() { - return app; - } /** * Remove all events from the Cinematic. 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 1a16f4fd1a..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 @@ -39,9 +39,6 @@ import com.jme3.export.JmeExporter; import com.jme3.export.JmeImporter; import com.jme3.export.OutputCapsule; -import com.jme3.util.clone.Cloner; -import com.jme3.util.clone.JmeCloneable; - import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -312,10 +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); - CinematicHandler cinematic = (CinematicHandler) ic.readSavable("cinematic", null); - if (cinematic != null) { - this.cinematic = cinematic; - } + cinematic = (CinematicHandler) ic.readSavable("cinematic", null); } /** 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 e92955cc82..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 @@ -75,8 +75,8 @@ public class AnimEvent extends AbstractCinematicEvent { * name of the animation layer on which the action will be played */ 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). @@ -194,6 +194,23 @@ public void onUpdate(float tpf) { // do nothing } + /** + * De-serialize this event from the specified importer, for example when + * loading from a J3O file. + * + * @param importer (not null) + * @throws IOException from the importer + */ + @Override + public void read(JmeImporter importer) throws IOException { + super.read(importer); + InputCapsule capsule = importer.getCapsule(this); + actionName = capsule.readString("actionName", ""); + composer = (AnimComposer) capsule.readSavable("composer", null); + layerName = capsule.readString("layerName", AnimComposer.DEFAULT_LAYER); + animRef = capsule.readString("animRef", null); + } + /** * Alter the speed of the animation. * @@ -254,23 +271,6 @@ public void setTime(float time) { } } - /** - * De-serialize this event from the specified importer, for example when - * loading from a J3O file. - * - * @param importer (not null) - * @throws IOException from the importer - */ - @Override - public void read(JmeImporter importer) throws IOException { - super.read(importer); - InputCapsule capsule = importer.getCapsule(this); - actionName = capsule.readString("actionName", ""); - composer = (AnimComposer) capsule.readSavable("composer", null); - layerName = capsule.readString("layerName", AnimComposer.DEFAULT_LAYER); - animRef = capsule.readString("animRef", null); - } - /** * Serialize this event to the specified exporter, for example when saving * to a J3O file. 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 fe22e5a020..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 @@ -51,17 +51,16 @@ public interface CinematicEvent extends Savable, CinematicBase { /** * initialize this event - * - * @param cinematic - * the cinematic + * @param app the application + * @param cinematic the cinematic */ 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 * this method should remove any reference to any external objects. 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 a177a383fc..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 @@ -156,8 +156,6 @@ public void initEvent(CinematicHandler cinematic) { audioNode = new AudioNode(cinematic.getAssetManager(), path, stream ? AudioData.DataType.Stream : AudioData.DataType.Buffer); audioNode.setPositional(false); - System.out.println("SoundEvent: loaded sound from " + path + this + audioNode + " for cinematic " - + getCinematic()); setLoopMode(loopMode); } @@ -174,10 +172,7 @@ public void setTime(float time) { @Override public void onPlay() { - System.out.println("SoundEvent: play sound from " + path + this + audioNode + " for cinematic " - + getCinematic()); audioNode.play(); - } @Override @@ -231,9 +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 = (AudioNode) ic.readSavable("audioNode", null); - if (audioNode != null) { - this.audioNode = audioNode; - } + audioNode = (AudioNode) ic.readSavable("audioNode", null); } }