多人 2D 游戏中的数据结构 (Java)

Data Structures in 2D-Games with Multiplayer (Java)

我有以下问题:

我编写了一个具有多人游戏功能的 2D 游戏。现在我将其他玩家数据和游戏对象存储在两个 ArrayList 中(世界以其他方式存储)。有时网络线程会发送无法应用的更新,因为游戏会绘制 Players/Game 对象 (java.util.ConcurrentModificationException)。因为这个绘图过程每秒发生大约 60 次(因为动画)问题经常出现(每 2 秒)。这是玩家 ArrayList 的代码:

抽签球员:

for (Player p : oPlayer) {
  if (p != null) {
    int x = (int) ((width / 2) + (p.x - getPlayerX()) * BLOCK_SIZE);
    int y = (int) ((height / 2) + (p.y - getPlayerY()) * BLOCK_SIZE);
    g.drawImage(onlinePlayer, x, y, BLOCK_SIZE, BLOCK_SIZE, null);
    FontMetrics fm = g.getFontMetrics();
    g.setColor(Color.DARK_GRAY);
    g.drawString(p.getName(), x + (BLOCK_SIZE / 2) - (fm.stringWidth(p.getName()) / 2), y - 5);
  }
}

在网络线程中编辑信息:

case "ADP": //add Player
  Game.oPlayer.add(new Player(message, id));
  sendX();
  sendY();
  break;
case "SPX": // set X
  for (Player p : Game.oPlayer) {
    if (p.getId() == id) {
      p.setX(Short.parseShort(message));
      break;
    }
  }
  break;
case "SPY": // set Y
  for (Player p : Game.oPlayer) {
    if (p.getId() == id) {
      p.setY(Short.parseShort(message));
      break;
    }
  }
  break;
case "PDI": // remove Player
  for (Player p : Game.oPlayer) {
    if (p.getId() == id) {
      Game.oPlayer.remove(p);
      break;
    }
  }
  break;

提前谢谢你:)

如果列表在一个线程和另一个线程中迭代或修改,您将得到 ConcurrentModficationException。在一般的用户界面应用程序中,修改模型数据仅限于单个线程,通常是用户界面线程,例如 Swing 的事件分发线程,或者 JavaFX 中的平台线程。

顺便说一句,对于 JavaFX,存在 game library 为游戏开发提供开箱即用的技术。一般来说,JavaFX 比 AWT 或 Swing 更适合图形密集型工作。

您尝试过使用 Vector 吗?它是集合的一部分并且是同步的。

这里发生的是,2 个线程在同一个列表上工作。
第一个是读取列表 (for (Player p : oPlayer) {),第二个是修改它 (Game.oPlayer.add(new Player(message, id));)。这使 oPlayer 列表进入(某种)"inconsistent" 状态。 Java 看到您修改了您正在阅读的内容并抛出此异常让您知道,该内容不符合 kosher。
可以找到有关 ConcurrentModificationExceptions 的更多信息 here

为了澄清,您深入了解了所谓的 Readers-writer problem。您有一个 reader(线程)读取 Game.oPlayer 的数据,还有一个写入器(线程)将数据写入 Game.oPlayer.

解决方案


同步关键字

synchronized关键字解释here。你会像这样使用它:

private final List<Player> players = ...;

public void addPlayer(Player player) {
    synchronized(players) {
        players.add(player);
    }
}

public void removePlayer(Player player) {
    synchronized(players) {
        players.remove(player);
    }
}

请注意,列表必须是最终的。此外,我使用的是本地属性而不是静态属性。用 Game.oPlayer 删除 players 以获得合适的解决方案。
这只允许 1 个线程访问 players.add()players.remove().


锁定

可以找到有关如何使用锁的信息 here

简单地说,你创建一个这样的块:

try {
lock.lock();
// work ..
} finally {
lock.unlock();
}

这样只有一个线程可以通过说 lock.lock() 访问工作部分。如果任何其他线程使用 lock.lock() 锁定了工作部分并且没有解锁它,则当前线程将等待直到 lock.unlock() 被调用。使用 try-finall 块来确保锁定已解锁,即使您的工作部分正在抛出一个 throwable。


此外,我建议像这样迭代 "copy" 个播放器列表:

List<Player> toIterate;
synchronized(players) {
    toIterate = new ArrayList<>(getPlayerList());
}
for(Player player : toIterate) {
    // work
}

或者像这样完全同步这部分:

synchronized(players) {
    for(Player player : players) {
        // work
    }
}

第一个为您提供该实例的副本,这基本上意味着它包含与原始列表相同的对象,但它不是同一个列表。它通过让更多线程自己工作 "list" 并完成它们的工作来帮助您,而不管当前的更新如何,因为第二个示例将在以下情况下阻塞:

  1. 任何线程都想阅读列表。
  2. 任何线程修改列表。

所以你只需要同步第一个例子中的复制部分。


甚至更进一步(不是您问题的一部分,但仍然可以使它变得更容易)我建议不要使用静态,正如您在 Game.oPlayer.[...] 中所述并查看 Dependency Injection .

您可以修改您的 Game-class 以提供方法 addPlayer(Player player);removePlayer(Player player);getPlayerList();Object Oriented 方式编写代码。
使用该设计,您可以轻松修改代码以处理新的并发问题。