如何修复 libGdx 中的游戏序列化无限循环?
How can I fix this game serialization infinite loop in libGdx?
我有 class GameState 如下;
public class GameState
{
private Array<Fleet> fleets;
private Array<Planet> planets;
private Array<Player> players;
//Constructors, methods, yada yada
}
我的 Fleet class 的非常简化的格式是;
public class 舰队
{
私人阵列船;
私有播放器所有者;
public Fleet(Player owner)
{
this.owner = owner;
this.ships = new Array<Ship>();
}
//Methods
}
简化Player
class;
public class Player
{
private Array<Fleet> fleets;
public Player()
{
fleets = new Array<Fleet>();
}
}
我使用 libGdx Json.toJson(state);
将我的游戏保存为 Json 格式。
我在直接引用中存储了我的信息,但这引起了一些小问题。第一个是 GameState
中数据的每个引用,在序列化时,都被 Json reader 读取为它自己的实例。所以,如果我序列化 GameState x
,然后反序列化它,它会在 GameState 中读取 Fleet
的 Array
,然后移动到 planets
,然后移动到 players
.每当它发现对存储在 fleets
中的实例的原始引用之一时,它就会将其视为自己的实例。
这意味着在加载新游戏之前,这两个引用将指向同一块内存,但在保存并重新加载后,它们将指向不同的内存块。此外,由于 Fleet
在其中保留了一个 Ship
的列表,并且每个 Ship
都包含对其父 fleet
的字段形式的引用,因此 Json
序列化器和反序列化器将因此陷入无限循环。
我尝试this,因为它似乎是最简单的方法,但正如内森在接受的答案中指出的那样,这不是最有效的方法。
如何重现这个问题?
游戏状态class
public class GameState
{
public Array<Player> players;
public GameState()
{
this.players = new Array<Player>();
players.add(new Player());
players.add(new Player());
}
}
玩家class
public class Player
{
public Array<Fleet> fleets;
public Player()
{
fleets = new Array<Fleet>();
}
public void addFleet(Fleet fleet)
{
fleets.add(fleet);
}
}
舰队class
public class Fleet()
{
public Player owner;
public Fleet(Player owner)
{
this.owner = owner;
this.owner.fleets.add(this);
}
}
主游戏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 infiniteLoopOrWhosebugErrorHappensHere = json.toJson(state);
state = json.fromJson(infiniteLoopOrWhosebugErrorHappensHere);
}
}
您应该由此或 Whosebug 错误得到一个无限循环。
这是深拷贝与浅拷贝的经典问题。有许多不同的技术可以处理这种情况,但对于游戏来说,一种简单的处理方法是为每个对象(如果您使用的是 Artemis 或 Ashley 等 ECS 框架,则为游戏实体)分配唯一标识符。
当你序列化对象时,不是嵌套其他对象,只是序列化一个id列表。反序列化时,您需要反序列化所有内容,然后将 id 扩展为实际的对象引用。
我在下面给出的是如何使用您提供的代码执行此操作的简单示例。
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 infiniteLoopOrWhosebugErrorHappensHere = json.toJson(state.toGameSaveState());
state = json.fromJson(GameSaveState.class, infiniteLoopOrWhosebugErrorHappensHere).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;
}
}
}
话虽如此,这个解决方案所做的只是用您拥有的代码修补一个基本问题。也就是说,您通过双向关系使代码紧密耦合。
有多种方法可以解决此问题。
您可以使关系成为单向的(玩家拥有许多舰队,但舰队没有对玩家的引用)。这将帮助您遵循典型的 OOP 技术为您的 类 建模。这也意味着查找哪个玩家拥有舰队的成本可能很高。您会根据所有权树而不是图表来考虑关系。这也会限制灵活性,但可能就足够了。
您可以对所有对象引用使用间接寻址,并将 ID 存储在基本对象中。然后,您将拥有一个查找服务(使用 HashMap)来存储映射到该对象的所有实体 ID。每当您需要该对象时,只需将 id 传递给服务即可。
您可以使用自定义序列化和反序列化,我认为 LibGdx 的 json 库支持它。你会想要使用 id 和浅引用,所以你需要一些特殊的机制来 save/restore 链接对象。但它会 trim 出额外的快照 类。
我有 class GameState 如下;
public class GameState
{
private Array<Fleet> fleets;
private Array<Planet> planets;
private Array<Player> players;
//Constructors, methods, yada yada
}
我的 Fleet class 的非常简化的格式是; public class 舰队 { 私人阵列船; 私有播放器所有者;
public Fleet(Player owner)
{
this.owner = owner;
this.ships = new Array<Ship>();
}
//Methods
}
简化Player
class;
public class Player
{
private Array<Fleet> fleets;
public Player()
{
fleets = new Array<Fleet>();
}
}
我使用 libGdx Json.toJson(state);
将我的游戏保存为 Json 格式。
我在直接引用中存储了我的信息,但这引起了一些小问题。第一个是 GameState
中数据的每个引用,在序列化时,都被 Json reader 读取为它自己的实例。所以,如果我序列化 GameState x
,然后反序列化它,它会在 GameState 中读取 Fleet
的 Array
,然后移动到 planets
,然后移动到 players
.每当它发现对存储在 fleets
中的实例的原始引用之一时,它就会将其视为自己的实例。
这意味着在加载新游戏之前,这两个引用将指向同一块内存,但在保存并重新加载后,它们将指向不同的内存块。此外,由于 Fleet
在其中保留了一个 Ship
的列表,并且每个 Ship
都包含对其父 fleet
的字段形式的引用,因此 Json
序列化器和反序列化器将因此陷入无限循环。
我尝试this,因为它似乎是最简单的方法,但正如内森在接受的答案中指出的那样,这不是最有效的方法。
如何重现这个问题?
游戏状态class
public class GameState
{
public Array<Player> players;
public GameState()
{
this.players = new Array<Player>();
players.add(new Player());
players.add(new Player());
}
}
玩家class
public class Player
{
public Array<Fleet> fleets;
public Player()
{
fleets = new Array<Fleet>();
}
public void addFleet(Fleet fleet)
{
fleets.add(fleet);
}
}
舰队class
public class Fleet()
{
public Player owner;
public Fleet(Player owner)
{
this.owner = owner;
this.owner.fleets.add(this);
}
}
主游戏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 infiniteLoopOrWhosebugErrorHappensHere = json.toJson(state);
state = json.fromJson(infiniteLoopOrWhosebugErrorHappensHere);
}
}
您应该由此或 Whosebug 错误得到一个无限循环。
这是深拷贝与浅拷贝的经典问题。有许多不同的技术可以处理这种情况,但对于游戏来说,一种简单的处理方法是为每个对象(如果您使用的是 Artemis 或 Ashley 等 ECS 框架,则为游戏实体)分配唯一标识符。
当你序列化对象时,不是嵌套其他对象,只是序列化一个id列表。反序列化时,您需要反序列化所有内容,然后将 id 扩展为实际的对象引用。
我在下面给出的是如何使用您提供的代码执行此操作的简单示例。
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 infiniteLoopOrWhosebugErrorHappensHere = json.toJson(state.toGameSaveState());
state = json.fromJson(GameSaveState.class, infiniteLoopOrWhosebugErrorHappensHere).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;
}
}
}
话虽如此,这个解决方案所做的只是用您拥有的代码修补一个基本问题。也就是说,您通过双向关系使代码紧密耦合。
有多种方法可以解决此问题。
您可以使关系成为单向的(玩家拥有许多舰队,但舰队没有对玩家的引用)。这将帮助您遵循典型的 OOP 技术为您的 类 建模。这也意味着查找哪个玩家拥有舰队的成本可能很高。您会根据所有权树而不是图表来考虑关系。这也会限制灵活性,但可能就足够了。
您可以对所有对象引用使用间接寻址,并将 ID 存储在基本对象中。然后,您将拥有一个查找服务(使用 HashMap)来存储映射到该对象的所有实体 ID。每当您需要该对象时,只需将 id 传递给服务即可。
您可以使用自定义序列化和反序列化,我认为 LibGdx 的 json 库支持它。你会想要使用 id 和浅引用,所以你需要一些特殊的机制来 save/restore 链接对象。但它会 trim 出额外的快照 类。