Saving/Loading Custom Control (extending AbstractControl, jME3.0)

Hi folks,

I`d like to save a custom Control that extends AbstractControl. But when i try to Load it the Game outputs a RuntimeException. I have no Idea, what the problem could be.

I reproduced the Problem in a simple TestCase. You can find the code and output below.

Main.java:

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.asset.plugins.FileLocator;
import com.jme3.export.binary.BinaryExporter;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.shape.Box;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;


public class Main extends SimpleApplication {

    public static void main(String[] args) {
        Main app = new Main();
        app.start();
    }

    @Override
    public void simpleInitApp() {
        Box b = new Box(1, 1, 1);
        Geometry boxGeometry = new Geometry("Box", b);

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        boxGeometry.setMaterial(mat);
        
        RotationControl rotationControl = new RotationControl();
        boxGeometry.addControl(rotationControl);

        Node sceneNode = new Node("sceneNode");
        
        
        sceneNode.attachChild(boxGeometry);     
        rootNode.attachChild(sceneNode);
        
        save(System.getProperty("user.home")+"\\Models\\someRandomNameThatIsNotOccupiedByAnyOtherFile_th45a6t4h.j3o");
        load(System.getProperty("user.home")+"\\Models\\","someRandomNameThatIsNotOccupiedByAnyOtherFile_th45a6t4h.j3o");
    }

    @Override
    public void simpleUpdate(float tpf) {
        
    }

    @Override
    public void simpleRender(RenderManager rm) {
        
    }
    
    public boolean save(String filePath){
        BinaryExporter exporter = BinaryExporter.getInstance();
        File file = new File(filePath);
        try {
            exporter.save(rootNode.getChild("sceneNode"), file);
        }
        catch (IOException ex) {
            Logger.getLogger(Main.class.getName()).log(Level.SEVERE, "Error: Failed to save game!", ex);
            return false;
        }     
        return true;
     }
    
     public boolean load(String parentPath, String fileName){
         assetManager.registerLocator(parentPath, FileLocator.class);
         rootNode.detachAllChildren();
         Node sceneNode = (Node) assetManager.loadModel(fileName);
         rootNode.attachChild(sceneNode);
         return true;         
     }
}

RotationControl.java:

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package mygame;

import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.control.AbstractControl;
import java.io.IOException;

public class RotationControl extends AbstractControl {
    private String someString="someText";
    
    @Override
    protected void controlUpdate(float tpf) {
        spatial.rotate(new Quaternion().fromAngleAxis(tpf*0.1f, Vector3f.UNIT_Y));
    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {
        //throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
    
    @Override
    public void write(JmeExporter ex) throws IOException{
        OutputCapsule capsule = ex.getCapsule(this);
        capsule.write(someString, "someString", "");
        someString = null;
        System.out.println("Saved");
    }
    @Override
    public void read(JmeImporter im) throws IOException{
        InputCapsule capsule = im.getCapsule(this);
        someString= capsule.readString("someString", "");
        System.out.println("Loaded");
    }
}

Output:

run:
Sep 06, 2015 10:07:33 PM java.util.prefs.WindowsPreferences <init>
Warnung: Could not open/create prefs root node Software\JavaSoft\Prefs at root 0x80000002. Windows RegCreateKeyEx(...) returned error code 5.
Sep 06, 2015 10:07:35 PM com.jme3.system.JmeDesktopSystem initialize
Information: Running on jMonkeyEngine 3.0.10
Sep 06, 2015 10:07:35 PM com.jme3.system.Natives extractNativeLibs
Information: Extraction Directory: C:\[...]\SaveLoadTestCase
Sep 06, 2015 10:07:36 PM com.jme3.system.lwjgl.LwjglContext printContextInitInfo
Information: Lwjgl 2.9.0 context running on thread LWJGL Renderer Thread
Sep 06, 2015 10:07:36 PM com.jme3.system.lwjgl.LwjglContext printContextInitInfo
Information: Adapter: igdumdim64
Sep 06, 2015 10:07:36 PM com.jme3.system.lwjgl.LwjglContext printContextInitInfo
Information: Driver Version: 10.18.10.4252
Sep 06, 2015 10:07:36 PM com.jme3.system.lwjgl.LwjglContext printContextInitInfo
Information: Vendor: Intel
Sep 06, 2015 10:07:36 PM com.jme3.system.lwjgl.LwjglContext printContextInitInfo
Information: OpenGL Version: 4.0.0 - Build 10.18.10.4252
Sep 06, 2015 10:07:36 PM com.jme3.system.lwjgl.LwjglContext printContextInitInfo
Information: Renderer: Intel(R) HD Graphics 4000
Sep 06, 2015 10:07:36 PM com.jme3.system.lwjgl.LwjglContext printContextInitInfo
Information: GLSL Ver: 4.00 - Build 10.18.10.4252
Sep 06, 2015 10:07:36 PM com.jme3.asset.AssetConfig loadText
Warnung: Cannot find loader com.jme3.scene.plugins.blender.BlenderModelLoader
WARNING: Found unknown Windows version: Windows 8
Attempting to use default windows plug-in.
Loading: net.java.games.input.DirectAndRawInputEnvironmentPlugin
Sep 06, 2015 10:07:36 PM com.jme3.audio.lwjgl.LwjglAudioRenderer initInThread
Information: Audio Device: OpenAL Soft
Sep 06, 2015 10:07:36 PM com.jme3.audio.lwjgl.LwjglAudioRenderer initInThread
Information: Audio Vendor: OpenAL Community
Sep 06, 2015 10:07:36 PM com.jme3.audio.lwjgl.LwjglAudioRenderer initInThread
Information: Audio Renderer: OpenAL Soft
Sep 06, 2015 10:07:36 PM com.jme3.audio.lwjgl.LwjglAudioRenderer initInThread
Information: Audio Version: 1.1 ALSOFT 1.15.1
Sep 06, 2015 10:07:36 PM com.jme3.audio.lwjgl.LwjglAudioRenderer initInThread
Information: AudioRenderer supports 64 channels
Sep 06, 2015 10:07:36 PM com.jme3.audio.lwjgl.LwjglAudioRenderer initInThread
Information: Audio effect extension version: 1.0
Sep 06, 2015 10:07:36 PM com.jme3.audio.lwjgl.LwjglAudioRenderer initInThread
Information: Audio max auxilary sends: 4
Saved
Loaded
Sep 06, 2015 10:07:37 PM com.jme3.app.Application handleError
Schwerwiegend: Uncaught exception thrown in Thread[LWJGL Renderer Thread,5,main]
java.lang.RuntimeException: Can't clone control for spatial
	at com.jme3.scene.control.AbstractControl.cloneForSpatial(AbstractControl.java:104)
	at com.jme3.scene.Spatial.clone(Spatial.java:1185)
	at com.jme3.scene.Geometry.clone(Geometry.java:440)
	at com.jme3.scene.Geometry.clone(Geometry.java:60)
	at com.jme3.scene.Spatial.clone(Spatial.java:1172)
	at com.jme3.scene.Node.clone(Node.java:565)
	at com.jme3.scene.Node.clone(Node.java:60)
	at com.jme3.scene.Spatial.clone(Spatial.java:1214)
	at com.jme3.scene.Spatial.clone(Spatial.java:66)
	at com.jme3.asset.CloneableAssetProcessor.createClone(CloneableAssetProcessor.java:48)
	at com.jme3.asset.DesktopAssetManager.loadAsset(DesktopAssetManager.java:327)
	at com.jme3.asset.DesktopAssetManager.loadModel(DesktopAssetManager.java:374)
	at com.jme3.asset.DesktopAssetManager.loadModel(DesktopAssetManager.java:378)
	at mygame.Main.load(Main.java:73)
	at mygame.Main.simpleInitApp(Main.java:44)
	at com.jme3.app.SimpleApplication.initialize(SimpleApplication.java:226)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.initInThread(LwjglAbstractDisplay.java:130)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:207)
	at java.lang.Thread.run(Thread.java:744)
Caused by: java.lang.CloneNotSupportedException: mygame.RotationControl
	at java.lang.Object.clone(Native Method)
	at com.jme3.scene.control.AbstractControl.cloneForSpatial(AbstractControl.java:99)
	... 18 more

BUILD SUCCESSFUL (total time: 7 seconds)

So I understand what to explain better… which part of this error is unclear?

At which point in the reading process is the Control cloned? Why is this necessary and for which reson does my Control not support cloning?

java.lang.RuntimeException: Can't clone control for spatial
	at com.jme3.scene.control.AbstractControl.cloneForSpatial(AbstractControl.java:104)

→ Make sure your Control at least overrides the cloneForSpatial() method and there use someString.
→ I guess the AssetManager firstly creates a control (or rather deserializes the saved one) and hands back a copy of it (for which you need to have cloneForSpatial implemented.

1 Like

The asset manager hands out clones.

So that if you load the same model/spatial/control again you get a new instance and not the same one you already loaded. AssetMangaer is a cache.

It doesn’t implement Cloneable I guess.

http://docs.oracle.com/javase/6/docs/api/java/lang/CloneNotSupportedException.html

1 Like

You almost never need to do this. In fact, we’d debated about a hundred times remove cloneForSpatial() since there is really only one place where it’s needed and it’s questionable even in that case.

Implement cloneable… make sure your clone() method does anything special you need done (if you need it done).

So the cloneForSpatial() only gets called when I don’t implement the Cloneable interface?
If so, that’s even cooler because iirc the shallow-cloning is done automatically (i.e. variables like health, mana and such would be cloned automatically then)?

Good News: Overriding cloneForSpatial() solved my problem. I didn’t even had to use someString, thanks.
Bad News: I have no idea why ^^

   @Override
    public Control cloneForSpatial(Spatial spatial) {
        final RotationControl control = new RotationControl();
        control.setSpatial(spatial);
        return control;
    }

If you extend AbstractControl then you get a default implementation of cloneForSpatial() that simply tries to use regular cloning with the clone() method. This was a compromise to keep people from having to implement cloneForSpatial() (versus just removing its requirement completely which I’ve unsuccessfully lobbied for a few times).

So, unless you have some extremely special spatial wiring issues (like the animation and skeleton controls do) then you don’t need to implement cloneForSpatial() if you extend AbstractControl. Just let the default one do its thing and implement regular Java clone() support.

Note: this will not clone any of your fields. It’s better to implement regular Java clone() method and implement Cloneable.

…that’s knowledge you can use everywhere versus JME’s hack-specific clone method.

1 Like

Yeah, implementing Cloneable works too. Thank you very much. I will use this in the future.

Simple:
Your someString doesn’t differ as of now.
Think of a Control which handles a NPC. It has fields like float health and such.

What you do, is creating a new NPC, but you don’t copy it’s maximum health, so as soon as you use any setters or custom constructors you have to copy those fields. In your example someString is constant though, so it doesn’t need to be cloned.

Keep in mind what pspeed says though: Learn how to use clone() you might need it for other use cases. Basically just adding “implements Cloneable” copies every “simple” (like non-Class) Variable (float, double, int, String, char) into your new control. That’s considered a shallow copy. It might even copy Class-Variables by invoking their clone.

You encounter trouble as soon as you have an array or list of a class. Imagine a List of NPCs.
The default behavior is to copy the list which means just copying the references. Imagine a List of citizens: You can copy that list but the citizens still stay the same.

What you need to implement there is a hard copy which means copying every citizen (and this depends on your Control)