Saving and Loading Games (.j3o)
Spatials (that is Nodes and Geometries) can contain audio and light nodes, particle emitters, controls, and user data (player score, health, inventory, etc). For your game distribution, you must convert all original models to a faster binary format. You save individual Spatials as well as scenes using com.jme3.export.binary.BinaryExporter
.
The jMonkeyEngine’s binary file format is called .j3o
. You can convert, view and edit .j3o files and their materials in the jMonkeyEngine SDK and compose scenes (this does not include editing meshes). For the conversion, you can either use the BinaryExporters, or a context menu in the SDK.
The jMonkeyEngine’s serialization system is the |
There is also a com.jme3.export.xml.XMLExporter and com.jme3.export.xml.XMLImporter that similarly converts jme3 spatials to an XML format. But you wouldn’t use that to load models at runtime (quite slow).
Saving a Node
The following example overrides stop()
in SimpleApplication to save the rootNode to a file when the user quits the application. The saved rootNode is a normal .j3o binary file that you can open in the SDK.
Note that when you save a model that has textures, the references to those textures are stored as absolute paths, so when loading the j3o file again, the textures have to be accessible at the exact location (relative to the assetmanager root, by default the |
/* This is called when the user quits the app. */
@Override
public void stop() {
String userHome = System.getProperty("user.home");
BinaryExporter exporter = BinaryExporter.getInstance();
File file = new File(userHome+"/Models/"+"MyModel.j3o");
try {
exporter.save(rootNode, file);
} catch (IOException ex) {
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, "Error: Failed to save game!", ex);
}
super.stop(); // continue quitting the game
}
Loading a Node
The following example overrides simpleInitApp()
in SimpleApplication to load Models/MyModel.j3o
when the game is initialized.
@Override
public void simpleInitApp() {
String userHome = System.getProperty("user.home");
assetManager.registerLocator(userHome, FileLocator.class);
Node loadedNode = (Node)assetManager.loadModel("Models/MyModel.j3o");
loadedNode.setName("loaded node");
rootNode.attachChild(loadedNode);
}
Here you see why we save user data inside spatials – so it can be saved and loaded together with the .j3o file. If you have game data outside Spatials, you have to remember to save() and load(), and get() and set() it yourself. |
Custom Savable Class
JME’s BinaryExporter can write standard Java objects (String, ArrayList, buffers, etc), JME objects (Savables, such as Material), and primitive data types (int, float, etc). If you are using any custom class together with a Spatial, then the custom class must implement the com.jme3.export.Savable
interface. There are two common cases where this is relevant:
-
The Spatial is carrying any Custom Controls.
Example: You used something likemySpatial.addControl(myControl);
-
The Spatial’s user data can contain a custom Java object.
Example: You used something likemySpatial.setUserData("inventory", myInventory);
If your custom classes (the user data or the Controls) do not implement Savable, then the BinaryImporter/BinaryExporter cannot save the Spatial!
So every time you create a custom Control or custom user data class, remember to implement Savable:
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.material.Material;
import java.io.IOException;
public class MyCustomClass implements Savable {
private int someIntValue; // some custom user data
private float someFloatValue; // some custom user data
private Material someJmeObject; // some custom user data
...
// your other code...
...
public void write(JmeExporter ex) throws IOException {
OutputCapsule capsule = ex.getCapsule(this);
capsule.write(someIntValue, "someIntValue", 1);
capsule.write(someFloatValue, "someFloatValue", 0f);
capsule.write(someJmeObject, "someJmeObject", new Material());
}
public void read(JmeImporter im) throws IOException {
InputCapsule capsule = im.getCapsule(this);
someIntValue = capsule.readInt( "someIntValue", 1);
someFloatValue = capsule.readFloat( "someFloatValue", 0f);
someJmeObject = capsule.readSavable("someJmeObject", new Material());
}
}
To make a custom class savable:
-
Implement
Savable
and add thewrite()
andread()
methods as shown in the example above. -
Do the following for each non-temporary class field:
-
Add one line that
write()
s the data to the JmeExport output capsule.-
Specify the variable to save, give it a String name (can be the same as the variable name), and specify a default value.
-
-
Add one line that
read…()
s the data to the JmeImport input capsule.-
On the left side of the assignment, specify the class field that you are restoring
-
On the right side, use the appropriate
capsule.read…()
method for the data type. Specify the String name of the variable (must be the same as you used in thewrite()
method), and again specify a default value.
-
-
As with all serialization, remember that if you ever change data types in custom classes, the updated read() methods will no longer be able to read your old files. Also there has to be a constructor that takes no Parameters. |
Default Value
The default value plays an important role in what data is saved to file.
public void write(int value, String name, int defVal) throws IOException {
if (value == defVal)
return;
writeAlias(name, BinaryClassField.INT);
write(value);
}
The write methods of the BinaryOutputCapsule.java class do not write the defVal
to file. Instead, they check to see if value
is equal to defVal
, and if so, will not write anything at all.
There are very good reasons to do this.
-
It takes less space if everything is a default value.
-
You may decide on new defaults later and your objects will automatically upgrade if they didn’t have specifically overridden values.
public int readInt(String name, int defVal) throws IOException {
BinaryClassField field = cObj.nameFields.get(name);
if (field == null || !fieldData.containsKey(field.alias))
return defVal;
return ((Integer) fieldData.get(field.alias)).intValue();
}
When reading your saved file, the BinaryInputCapsule.java class will see that the name
field is null
and this is when the defVal is set.
If you rely on the compiler to initialize class or instance variables for you, this can lead too unintended consequences. For example:
If you let the compiler initialize
Now when |