8

我正在 Java 平台上开发一个实时战略游戏克隆,我有一些关于放置位置和如何管理游戏状态的概念性问题。游戏使用 Swing/Java2D 作为渲染。在当前的开发阶段,没有模拟和人工智能,只有用户能够改变游戏的状态(例如,建造/拆除建筑物、添加-移除生产线、组装车队和设备)。因此,可以在事件调度线程中执行游戏状态操作,而无需任何渲染查找。游戏状态还用于向用户显示各种聚合信息。

但是,由于我需要引入模拟(例如,建筑进度、人口变化、舰队移动、制造过程等),在 Timer 和 EDT 中更改游戏状态肯定会减慢渲染速度。

假设每 500 毫秒执行一次模拟/AI 操作,我使用 SwingWorker 进行大约 250 毫秒长度的计算。我如何确保模拟和可能的用户交互之间的游戏状态读取不存在竞争条件?

我知道模拟结果(即少量数据)可以通过 SwingUtilities.invokeLater() 调用有效地移回 EDT。

游戏状态模型似乎太复杂以至于无法在任何地方都使用不可变值类。

是否有相对正确的方法来消除这种读取竞争条件?也许在每个计时器滴答声上进行完整/部分游戏状态克隆,或者将游戏状态的生存空间从 EDT 更改为其他线程?

更新:(根据我给出的评论)该游戏由 13 名 AI 控制的玩家、1 名人类玩家组成,并且有大约 10000 个游戏对象(行星、建筑物、设备、研究等)。例如,游戏对象具有以下属性:

世界(行星、玩家、舰队……)
行星(位置、所有者、人口、类型、
    地图、建筑物、税收、分配……)
建筑(位置、启用、能源、工人、健康……)

在一个场景中,用户在这个星球上建造了一座新建筑。这是在 EDT 中执行的,因为需要更改地图和建筑物集合。与此同时,每 500 毫秒运行一次模拟,以计算所有游戏星球上建筑物的能量分配,这需要遍历建筑物集合以进行统计收集。如果计算了分配,则将其提交给 EDT,并分配每个建筑物的能量场。

只有人类玩家交互具有此属性,因为 AI 计算的结果无论如何都会应用于 EDT 中的结构。

通常,75% 的对象属性是静态的,仅用于渲染。其余部分可以通过用户交互或模拟/人工智能决策进行更改。还确保在前一个步骤写回所有更改之前不会启动新的模拟/AI步骤。

我的目标是:

  • 避免延迟用户交互,例如用户将建筑物放置在地球上,并且仅在 0.5s 后才获得视觉反馈
  • 避免用计算、锁等待等阻塞 EDT。
  • 避免集合遍历和修改、属性更改的并发问题

选项:

  • 细粒度对象锁定
  • 不可变集合
  • 挥发性领域
  • 部分快照

所有这些对模型和游戏都有优点、缺点和原因。

更新2:我说的是这个游戏。我的克隆在这里。屏幕截图可能有助于想象渲染和数据模型的交互。

更新 3:

我将尝试提供一个小代码示例来澄清我的问题,因为从评论中似乎它被误解了:

List<GameObject> largeListOfGameObjects = ...
List<Building> preFilteredListOfBuildings = ...
// In EDT
public void onAddBuildingClicked() {
    Building b = new Building(100 /* kW */);
    largeListOfGameObjects.add(b);
    preFilteredListOfBuildings.add(b);
}
// In EDT
public void paint(Graphics g) {
    int y = 0;
    for (Building b : preFilteredListOfBuildings) {
        g.drawString(Integer.toString(b.powerAssigned), 0, y);
        y += 20;
    }
}
// In EDT
public void assignPowerTo(Building b, int amount) {
    b.powerAssigned = amount;
}
// In simulation thread
public void distributePower() {
    int sum = 0;
    for (Building b : preFilteredListOfBuildings) {
        sum += b.powerRequired;
    }
    final int alloc = sum / (preFilteredListOfBuildings.size() + 1);
    for (final Building b : preFilteredListOfBuildings) {
        SwingUtilities.invokeLater(=> assignPowerTo(b, alloc));            
    }
}

所以重叠是在onAddBuildingClicked() 和distributePower() 之间。现在想象一下,在游戏模型的各个部分之间有 50 个这样的重叠。

4

8 回答 8

3

这听起来可以从客户端/服务器方法中受益:

播放器是客户端——交互和渲染发生在这一端。所以玩家按下一个按钮,请求就会发送到服务器。来自服务器的回复回来了,玩家的状态被更新了。在这些事情发生之间的任何时候,屏幕都可以重新绘制,它反映了客户端当前所知道的游戏状态。

人工智能同样是一个客户端——它相当于一个机器人。

模拟是服务器。它在不同时间从其客户那里获取更新并更新世界的状态,然后将这些更新发送给适当的每个人。这就是它与您的情况相关的地方:模拟/AI 需要一个静态的世界,许多事情同时发生。服务器可以简单地将更改请求排队并在将更新发送回客户端之前应用它们。所以就服务器而言,游戏世界实际上并没有实时变化,只要服务器决定它是,它就会发生变化。

最后,在客户端,您可以通过进行一些快速的近似计算并显示结果(因此满足即时需求)然后在服务器运行时显示更正确的结果来防止按下按钮和看到结果之间的延迟和你说话。

请注意,这实际上不必以 TCP/IP over-the-internet 的方式实现,只是从这些方面考虑它会有所帮助。

或者,您可以将在模拟期间保持数据一致性的责任放在数据库上,因为它们已经在构建时考虑了锁定和一致性。像 sqlite 这样的东西可以作为非网络解决方案的一部分。

于 2009-06-30T19:15:30.420 回答
0

不确定我是否完全理解您正在寻找的行为,但听起来您需要状态更改线程/队列之类的东西,因此所有状态更改都由单个线程处理。

为您的状态更改队列创建一个 api,例如 SwingUtilities.invokeLater() 和/或 SwingUtilities.invokeAndWait() 以处理您的状态更改请求。

我认为这如何反映在 gui 中取决于您正在寻找的行为。即无法取款,因为当前状态为$0,或者在处理取款请求时向用户弹回账户为空。(可能不是用那个术语;-))

于 2009-06-11T17:18:02.620 回答
0

最简单的方法是使模拟足够快以在 EDT 中运行。更喜欢有效的程序!

对于双线程模型,我建议将域模型与渲染模型同步。渲染模型应该保留来自域模型的数据。

更新:在模拟线程中锁定渲染模型。遍历渲染模型更新与预期不同的地方更新渲染模型。完成遍历后,解锁渲染模型并安排重绘。请注意,在这种方法中,您不需要大量的听众。

渲染模型可以有不同的深度。在一个极端情况下,它可能是一个图像,更新操作只是用新的图像对象替换单个引用(这不会很好地处理,例如,调整大小或其他表面交互)。您可能不会费心检查一个项目是否有变化,而只是更新一切。

于 2009-06-11T17:35:24.060 回答
0

如果更改游戏状态很快(一旦您知道将其更改为什么),您可以像其他 Swing 模型一样对待游戏状态,只在 EDT 中更改或查看状态。如果更改游戏状态不是很快,那么您可以同步状态更改并在 swing worker/timer(但不是 EDT)中进行,或者您可以在与 EDT 类似的单独线程中进行(此时您看看使用 aBlockingQueue来处理变更请求)。如果 UI 不必从游戏状态中检索信息,而是通过侦听器或观察者发送渲染更改,则最后一个更有用。

于 2009-06-11T19:35:39.653 回答
0

是否可以增量更新游戏状态并且仍然有一个一致的模型?例如,在渲染/用户更新之间重新计算行星/玩家/舰队对象的子集。

如果是这样,您可以在 EDT 中运行增量更新,在允许 EDT 处理用户输入和渲染之前只计算一小部分状态。

在 EDT 中的每次增量更新之后,您需要记住有多少模型需要更新,并在 EDT 上安排一个新的 SwingWorker 以在执行任何待处理的用户输入和渲染后继续此处理。

这应该允许您避免复制或锁定游戏模型,同时仍保持用户交互响应。

于 2009-06-14T10:42:14.887 回答
0

我认为您不应该让 World 存储任何数据或更改任何对象本身,它应该只用于维护对对象的引用,并且当需要更改该对象时,让进行更改的玩家直接更改它。在这种情况下,您唯一需要做的就是同步游戏世界中的每个对象,以便当玩家进行更改时,其他玩家无法这样做。这是我在想的一个例子:

玩家 A 需要了解行星,因此它向 World 询问该行星(如何取决于您的实现)。World 返回对玩家 A 请求的 Planet 对象的引用。玩家 A 决定做出改变,所以它这样做了。假设它增加了一座建筑物。将建筑物添加到星球的方法是同步的,因此一次只有一个玩家可以这样做。该建筑将跟踪自己的建造时间(如果有的话),因此行星的添加建造方法将几乎立即被释放。这样,多个玩家可以同时询问同一个星球上的信息而不会相互影响,玩家几乎可以同时添加建筑物而不会出现太多延迟。如果两个玩家正在寻找放置建筑物的地方(如果这是您游戏的一部分),

很抱歉,如果这不能回答您的问题,我不确定我是否理解正确。

于 2009-06-19T03:58:42.780 回答
0

如何实现管道和过滤器架构。如果过滤器不够快,管道将过滤器连接在一起并排队请求。处理发生在过滤器内部。第一个过滤器是 AI 引擎,而渲染引擎由一组后续过滤器实现。

在每个计时器滴答声中,根据所有输入(时间也是一个输入)和插入第一个管道的副本计算新的动态世界状态。

在最简单的情况下,您的渲染引擎被实现为单个过滤器。它只是从输入管道获取状态快照并将其与静态状态一起呈现。在现场游戏中,如果管道中有多个状态,渲染引擎可能希望跳过状态,而如果您正在执行基准测试或输出视频,您将希望渲染每个状态。

您可以将渲染引擎分解成的过滤器越多,并行性就越好。也许甚至可以分解 AI 引擎,例如,您可能希望将动态状态分为快速变化和缓慢变化的状态。

这种架构为您提供了良好的并行性,而无需大量同步。

这种架构的一个问题是垃圾收集会频繁运行,每次都会冻结所有线程,可能会扼杀从多线程获得的任何优势。

于 2009-06-21T16:49:29.153 回答
0

看起来您需要一个优先队列来对模型进行更新,其中用户的更新优先于模拟和其他输入的更新。我听到你说的是,用户总是需要对他的行为的即时反馈,而其他输入(模拟,否则)可能需要比一个模拟步骤更长的工作人员。然后在优先队列上同步。

于 2009-06-27T12:52:10.943 回答