From a0ac32d5244c30be778bec17d7066809e343a6f8 Mon Sep 17 00:00:00 2001 From: Wyatt Gillette Date: Mon, 22 Sep 2025 14:41:07 +0200 Subject: [PATCH 1/2] 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 2/2] 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