patternjavaMinor
Custom Serialization of Game World
Viewed 0 times
customgameserializationworld
Problem
I'm working with libGDX for a game, and over the last few days I have tried a few different types of serialization before settling on a custom serialization implementation. XML had huge file sizes, JSON was a bit better but unfortunately gave me problems on iOS, and
@SimonAndreForsberg linked this article to me about using static factory methods in Java, and I agree that they are important here to handle the exceptions that can be thrown during object construction if something goes wrong.
The serialization itself is very simple. I simply manually encode all the necessary information from each object, and manually decode it when loading the game. The only complicated part of this is that I pass the
The major drawback of this serialization approach is that any small change to the classes (such as adding a new instance variable for a new feature) will require a change to the serialization, which will then break any old saves. I don't like this, but it may well be a necessary tradeoff. This serialization is working on all platforms, and the file size is 1/30th of what it was with XML or JSON.
I will start at the top and show a couple of levels of serialization. Each class that needs to be serialized passes the stream to any objects that it possesses that need to be serialized.
For reference the
This is called when the save button is pressed:
```
public void saveGame() throws IOException {
switch(Gdx.app.getT
Serializable would not work because if I extended that with my classes, they would not compile for the GWT version of the game. Even though the HTML/GWT version of the game will not be able to have saves, I still wanted the same code to work on all platforms without any changes.@SimonAndreForsberg linked this article to me about using static factory methods in Java, and I agree that they are important here to handle the exceptions that can be thrown during object construction if something goes wrong.
The serialization itself is very simple. I simply manually encode all the necessary information from each object, and manually decode it when loading the game. The only complicated part of this is that I pass the
DataOutputStream and DataInputStream objects into the static factory methods of the classes in order to do the coding and decoding. The major drawback of this serialization approach is that any small change to the classes (such as adding a new instance variable for a new feature) will require a change to the serialization, which will then break any old saves. I don't like this, but it may well be a necessary tradeoff. This serialization is working on all platforms, and the file size is 1/30th of what it was with XML or JSON.
I will start at the top and show a couple of levels of serialization. Each class that needs to be serialized passes the stream to any objects that it possesses that need to be serialized.
For reference the
World contains 50x50 (2500) Regions, which each contain 11x7 (77) Tiles. After serialization the world file is about 1.2 megabytes.This is called when the save button is pressed:
```
public void saveGame() throws IOException {
switch(Gdx.app.getT
Solution
Serializing ordinal values
When relying the ordinal value of an enum, remember that you cannot change the order of these enums, that will break things. It has happened before.
I'd personally give each enum value it's own
Code duplication for reading strings
You have a method to write a String to a
Why do you not also have a method to read a string? This is duplicated code:
That could be:
setTileForPosition
First of all, does this really need to be public? I would make that
Additionally, if it is changed to private, you won't need it as a method at all, as you can change this:
to:
Data structure for 2D
You are currently storing a region using a
As a region is always 2D in size, you can instead store it using a 2D array,
When reading you can do:
This will save you some more bytes, as there is no real reason to write the position of each tile. Instead, loop over them in a specific order, the one I find most common, and would recommend, is:
if (regionBiomeType == BiomeType.NETHER.ordinal()) {When relying the ordinal value of an enum, remember that you cannot change the order of these enums, that will break things. It has happened before.
I'd personally give each enum value it's own
int id, which you then can use for serializing and deserializing.enum BiomeType {
NORMAL(42), BIOME(23);
private final int id;
private BiomeType(int id) {
this.id = id;
}
public static BiomeType forId(int id) {
// loop through all the biomes and find the one with the matching id
}
}Code duplication for reading strings
You have a method to write a String to a
DataOutputStream, that is a good one. Whoever suggested that is a genius.Why do you not also have a method to read a string? This is duplicated code:
if (ownerNameLength > 0) {
byte [] ownerNameBytes = new byte[ownerNameLength];
in.read(ownerNameBytes);
region.setOwnerName(new String(ownerNameBytes, "UTF-8"));
}That could be:
region.name = BZSerializationTools.readString(in);
region.setOwnerName(BZSerializationTools.readString(in));setTileForPosition
First of all, does this really need to be public? I would make that
private if possible, or default-visibility (package-private) if not possible.public void setTileForPosition(MapPoint position, Tile tile) {
this.regionMap.put(position, tile);
}Additionally, if it is changed to private, you won't need it as a method at all, as you can change this:
region.setTileForPosition(loadedTile.position(), loadedTile);to:
region.regionMap.put(loadedTile.position(), loadedTile);Data structure for 2D
You are currently storing a region using a
Map, and serializing it by using the size of the region map:out.write(this.regionMap.values().size());
for (Tile tile : this.regionMap.values()) {
tile.customEncode(out);
}As a region is always 2D in size, you can instead store it using a 2D array,
Tile[][], and serialize it using the following approach:- write size (width, height) of region
- loop on each tile in specific order:
- write tile, except its position
When reading you can do:
- read size (width, height)
- loop on each index in the
Tile[][]array:
- read tile, except its position
This will save you some more bytes, as there is no real reason to write the position of each tile. Instead, loop over them in a specific order, the one I find most common, and would recommend, is:
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
// read or write tile[x][y] (or [y][x] depending on how you want to store that)
}
}Code Snippets
if (regionBiomeType == BiomeType.NETHER.ordinal()) {enum BiomeType {
NORMAL(42), BIOME(23);
private final int id;
private BiomeType(int id) {
this.id = id;
}
public static BiomeType forId(int id) {
// loop through all the biomes and find the one with the matching id
}
}if (ownerNameLength > 0) {
byte [] ownerNameBytes = new byte[ownerNameLength];
in.read(ownerNameBytes);
region.setOwnerName(new String(ownerNameBytes, "UTF-8"));
}region.name = BZSerializationTools.readString(in);
region.setOwnerName(BZSerializationTools.readString(in));public void setTileForPosition(MapPoint position, Tile tile) {
this.regionMap.put(position, tile);
}Context
StackExchange Code Review Q#84033, answer score: 4
Revisions (0)
No revisions yet.