如何在libGdx中修复此游戏序列化无限循环?

时间:2022-02-12 07:52:56

I have class GameState as follows;

我有类GameState如下;

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;

我的Fleet类的非常简化的格式是;公共级舰队{私人阵列船;私人球员老板;

    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.

我使用libGdx Json.toJson(state);将我的游戏保存为Json格式。

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.

我将我的信息存储在直接引用中,但这引起了一些小问题。第一个是,当序列化时,每个对GameState中数据的引用都被Json读取器读取为它自己的实例。因此,如果我序列化GameState x,然后对其进行反序列化,它将在GameState中读取我的Fleet's数组并移动到行星上,然后转移到玩家身上。每当它发现对存储在车队中的实例的原始引用之一时,它就会将其视为自己的实例。

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.

这意味着在加载新游戏之前,两个引用将指向同一块内存,但在保存和重新加载后,它们将指向单独的内存块。此外,由于Fleet保留了Ship的列表,并且每个Ship包含一个字段形式的引用到其父队,因此Json序列化器和解串器将被抛入无限循环。

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.

我试图这样做,因为它似乎是最简单的方法,但正如内森在接受的答案中指出的那样,而不是最有效的方法。

How to recreate this problem in short?

GameState class

GameState类

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

MainGame类

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 infiniteLoopOr*ErrorHappensHere = json.toJson(state);
        state = json.fromJson(infiniteLoopOr*ErrorHappensHere);
    }
}

You should get an infinite loop from this or a * error.

您应该从此处获得无限循环或*错误。

1 个解决方案

#1


2  

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).

这是深拷贝与浅拷贝的典型问题。有许多不同的技术可以处理这种情况,但对于游戏来说,处理这种情况的一种简单方法是为每个对象(或游戏实体,如果您使用的是Artemis或Ashley等ECS框架)分配唯一标识符。

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.

序列化对象时,只需序列化一个id列表,而不是嵌套其他对象。反序列化时,您需要反序列化所有内容,然后将ID扩展为实际的对象引用。

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 infiniteLoopOr*ErrorHappensHere = json.toJson(state.toGameSaveState());
        state = json.fromJson(GameSaveState.class, infiniteLoopOr*ErrorHappensHere).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.

你可以使关系单向(玩家拥有许多舰队,但舰队没有参考玩家的参考)。这将帮助您遵循您为类建模所知的典型OOP技术。这也意味着查找哪个玩家拥有一支舰队可能代价高昂。您将根据所有权树而不是图表来考虑关系。这也可以限制灵活性,但这可能就足够了。

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.

您可以对所有对象引用使用间接,并将id存储在基本对象中。然后,您将拥有一个查找服务(使用HashMap),该服务存储映射到该对象的所有实体ID。只要您想要该对象,您只需将id传递给服务即可。

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.

您可以使用LibGdx的json库支持的自定义序列化和反序列化。你想使用id和浅引用,所以你需要一些特殊的机制来保存/恢复链接的对象。但它会削减额外的快照类。

#1


2  

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).

这是深拷贝与浅拷贝的典型问题。有许多不同的技术可以处理这种情况,但对于游戏来说,处理这种情况的一种简单方法是为每个对象(或游戏实体,如果您使用的是Artemis或Ashley等ECS框架)分配唯一标识符。

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.

序列化对象时,只需序列化一个id列表,而不是嵌套其他对象。反序列化时,您需要反序列化所有内容,然后将ID扩展为实际的对象引用。

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 infiniteLoopOr*ErrorHappensHere = json.toJson(state.toGameSaveState());
        state = json.fromJson(GameSaveState.class, infiniteLoopOr*ErrorHappensHere).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.

你可以使关系单向(玩家拥有许多舰队,但舰队没有参考玩家的参考)。这将帮助您遵循您为类建模所知的典型OOP技术。这也意味着查找哪个玩家拥有一支舰队可能代价高昂。您将根据所有权树而不是图表来考虑关系。这也可以限制灵活性,但这可能就足够了。

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.

您可以对所有对象引用使用间接,并将id存储在基本对象中。然后,您将拥有一个查找服务(使用HashMap),该服务存储映射到该对象的所有实体ID。只要您想要该对象,您只需将id传递给服务即可。

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.

您可以使用LibGdx的json库支持的自定义序列化和反序列化。你想使用id和浅引用,所以你需要一些特殊的机制来保存/恢复链接的对象。但它会削减额外的快照类。