I have class GameState as follows;
public class GameState
{
private Array<Fleet> fleets;
private Array<Planet> planets;
private Array<Player> players;
//Constructors, methods, yada yada
}
The very simplified format of my Fleet class is; public class Fleet { private Array ships; private Player owner;
public Fleet(Player owner)
{
this.owner = owner;
this.ships = new Array<Ship>();
}
//Methods
}
Simplified Player
class;
public class Player
{
private Array<Fleet> fleets;
public Player()
{
fleets = new Array<Fleet>();
}
}
I use the libGdx Json.toJson(state);
to save my game into Json format.
I stored my information in direct references, but this caused some minor problems. The first is that each reference to the data in GameState
, when serialized, is read by the Json reader as being its own instance. So, if I serialized GameState x
, then deserialized it, it would read my Array
of Fleet
's in GameState and move onto planets
, then move onto players
. Whenever it found one of the original references to an instance stored in fleets
it would treat it as its own instance.
That means that before loading a new game, the two references would point to the same piece of memory, but after saving and reloading they would point to separate pieces of memory. Also, since Fleet
keeps a list of Ship
's in it and each Ship
contains a reference in the form of a field to its parent fleet
, the Json
serializer and deserializer would be thrown into an infinite loop because of this.
I tried to do this as it seemed to be the easiest approach, but as Nathan pointed out in the accepted answer, not the most efficient approach.
GameState class
public class GameState
{
public Array<Player> players;
public GameState()
{
this.players = new Array<Player>();
players.add(new Player());
players.add(new Player());
}
}
Player class
public class Player
{
public Array<Fleet> fleets;
public Player()
{
fleets = new Array<Fleet>();
}
public void addFleet(Fleet fleet)
{
fleets.add(fleet);
}
}
Fleet class
public class Fleet()
{
public Player owner;
public Fleet(Player owner)
{
this.owner = owner;
this.owner.fleets.add(this);
}
}
MainGame class
public class MainGame extends Game
{
@Override
public void create()
{
GameState state = new GameState();
state.fleets.add(new Fleet(state.players.get(0)));
state.fleets.add(new Fleet(state.players.get(1)));
Json json = new Json();
String infiniteLoopOrStackOverflowErrorHappensHere = json.toJson(state);
state = json.fromJson(infiniteLoopOrStackOverflowErrorHappensHere);
}
}
You should get an infinite loop from this or a StackOverflow error.
This is a classic problem with deep copies vs. shallow copies. There's many different techniques for handling this sort of situation, but for a game, a simple way of handling this is to assign unique identifiers to each object (or game entity if you are using an ECS framework like Artemis or Ashley).
When you serialize objects, instead of nesting other objects, just serialize a list of ids. When you deserialize, you'll need to deserialize everything and then expand the ids into actual object references.
What I've put below is a simple example of how to do this with the code you provided.
public class MainGame extends ApplicationAdapter {
@Override
public void create() {
final Player player0 = new Player();
final Player player1 = new Player();
final Fleet fleet0 = new Fleet(player0);
player0.fleets.add(fleet0);
final Fleet fleet1 = new Fleet(player1);
player1.fleets.add(fleet1);
GameState state = new GameState();
state.players.add(player0);
state.players.add(player1);
state.fleets.add(fleet0);
state.fleets.add(fleet1);
final Json json = new Json();
final String infiniteLoopOrStackOverflowErrorHappensHere = json.toJson(state.toGameSaveState());
state = json.fromJson(GameSaveState.class, infiniteLoopOrStackOverflowErrorHappensHere).toGameState();
}
}
public abstract class BaseEntity {
private static long idCounter = 0;
public final long id;
BaseEntity() {
this(idCounter++);
}
BaseEntity(final long id) {
this.id = id;
}
}
public abstract class BaseSnapshot {
public final long id;
BaseSnapshot(final long id) {
this.id = id;
}
}
public class Fleet extends BaseEntity {
public Player owner;
Fleet(final long id) {
super(id);
}
public Fleet(final Player owner) {
this.owner = owner;
//this.owner.fleets.add(this); --> Removed because this is a side-effect!
}
public FleetSnapshot toSnapshot() {
return new FleetSnapshot(id, owner.id);
}
public static class FleetSnapshot extends BaseSnapshot {
public final long ownerId;
//Required for serialization
FleetSnapshot() {
super(-1);
ownerId = -1;
}
public FleetSnapshot(final long id, final long ownerId) {
super(id);
this.ownerId = ownerId;
}
public Fleet toFleet(final Map<Long, BaseEntity> entitiesById) {
final Fleet fleet = (Fleet)entitiesById.get(id);
fleet.owner = (Player)entitiesById.get(ownerId);
return fleet;
}
}
}
public class GameSaveState {
public final Array<PlayerSnapshot> playerSnapshots;
public final Array<FleetSnapshot> fleetSnapshots;
//required for serialization
GameSaveState() {
playerSnapshots = null;
fleetSnapshots = null;
}
public GameSaveState(final Array<PlayerSnapshot> playerSnapshots, final Array<FleetSnapshot> fleetSnapshots) {
this.playerSnapshots = playerSnapshots;
this.fleetSnapshots = fleetSnapshots;
}
public GameState toGameState() {
final Map<Long, BaseEntity> entitiesById = constructEntitiesByIdMap();
final GameState restoredState = new GameState();
restoredState.players = restorePlayerEntities(entitiesById);
restoredState.fleets = restoreFleetEntities(entitiesById);
return restoredState;
}
private Map<Long, BaseEntity> constructEntitiesByIdMap() {
final Map<Long, BaseEntity> entitiesById = new HashMap<Long, BaseEntity>();
for (final PlayerSnapshot playerSnapshot : playerSnapshots) {
final Player player = new Player(playerSnapshot.id);
entitiesById.put(player.id, player);
}
for (final FleetSnapshot fleetSnapshot : fleetSnapshots) {
final Fleet fleet = new Fleet(fleetSnapshot.id);
entitiesById.put(fleet.id, fleet);
}
return entitiesById;
}
private Array<Player> restorePlayerEntities(final Map<Long, BaseEntity> entitiesById) {
final Array<Player> restoredPlayers = new Array<Player>(playerSnapshots.size);
for (final PlayerSnapshot playerSnapshot : playerSnapshots) {
restoredPlayers.add(playerSnapshot.toPlayer(entitiesById));
}
return restoredPlayers;
}
private Array<Fleet> restoreFleetEntities(final Map<Long, BaseEntity> entitiesById) {
final Array<Fleet> restoredFleets = new Array<Fleet>(fleetSnapshots.size);
for (final FleetSnapshot fleetSnapshot : fleetSnapshots) {
restoredFleets.add(fleetSnapshot.toFleet(entitiesById));
}
return restoredFleets;
}
}
public class GameState {
public Array<Player> players = new Array<Player>();
public Array<Fleet> fleets = new Array<Fleet>();
public GameSaveState toGameSaveState() {
final Array<PlayerSnapshot> playerSnapshots = new Array<PlayerSnapshot>(players.size);
final Array<FleetSnapshot> fleetSnapshots = new Array<FleetSnapshot>(fleets.size);
for (final Player player : players) {
playerSnapshots.add(player.toSnapshot());
}
for (final Fleet fleet : fleets) {
fleetSnapshots.add(fleet.toSnapshot());
}
return new GameSaveState(playerSnapshots, fleetSnapshots);
}
}
public class Player extends BaseEntity {
public Array<Fleet> fleets = new Array<Fleet>();
public Player () {}
Player (final long id) {
super(id);
}
public PlayerSnapshot toSnapshot() {
final Array<Long> fleetIds = new Array<Long>(fleets.size);
for(final Fleet fleet : fleets) {
fleetIds.add(fleet.id);
}
return new PlayerSnapshot(id, fleetIds);
}
public static class PlayerSnapshot extends BaseSnapshot {
public final Array<Long> fleetIds;
//Required for serialization
PlayerSnapshot() {
super(-1);
fleetIds = null;
}
public PlayerSnapshot(final long id, final Array<Long> fleetIds) {
super(id);
this.fleetIds = fleetIds;
}
public Player toPlayer(final Map<Long, BaseEntity> entitiesById) {
final Player restoredPlayer = (Player)entitiesById.get(id);
for (final long fleetId : fleetIds) {
restoredPlayer.fleets.add((Fleet)entitiesById.get(fleetId));
}
return restoredPlayer;
}
}
}
This being said, all this solution is doing is patching a fundamental problem with the code you have. That is, you are making your code tightly coupled by having bidirectional relationships.
There's different ways you could solve this problem.
You could make the relationships unidirectional (a Player owns many Fleets, but the Fleets don't have a reference back to a Player). This will help you follow typical OOP techniques you know for modelling your classes. It also means that looking up which player owns a Fleet could be costly. You would be thinking of relationships in terms of a tree of ownership, rather than a graph. This can also limit flexibility, but it might be sufficient.
You could use indirection for all your object references and just store ids in the basic object. Then you'd have a lookup service (using a HashMap) that stores all the entity ids mapped to the object. Whenever you want the object, you just pass the id into the service.
You could use custom serialization and deserialization, which is supported by LibGdx's json library, I think. You'd want to use ids and shallow references, so you'd need some special mechanism to save/restore linked objects. But it would trim out the extra snapshot classes.