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

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

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

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

游戏状态模型似乎太复杂了,无法在任何地方使用不可变的值类。

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

更新: (从我发表的评论中)游戏具有13名AI受控玩家,1名人类玩家,并且具有大约10000个游戏对象(行星,建筑物,设备,研究等)。例如,游戏对象具有以下属性:

World (Planets, Players, Fleets, ...)
Planet (location, owner, population, type, 
    map, buildings, taxation, allocation, ...)
Building (location, enabled, energy, worker, health, ...)

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

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

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

我的目标是:

  • 避免延迟用户交互,例如用户将建筑物放置到地球上,仅在 0.5 秒后即可获得视觉反馈
  • 避免因计算、锁等待等阻塞 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() 和 allocatePower() 之间。现在想象一下这样的情况:游戏模型的各个部分之间有 50 个此类重叠。

有帮助吗?

解决方案

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

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

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

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

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

请注意,这实际上不必以 TCP/IP 互联网上的方式实现,只是有助于以这些术语来思考它。

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

其他提示

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

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

我认为这如何反映在 GUI 中取决于您正在寻找的行为。IE。无法取款,因为当前状态为 0 美元,或者在处理取款请求时向用户弹回帐户为空的信息。(可能不使用该术语;-))

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

这种架构的一个问题是,垃圾收集每次都会频繁地运行,从而冻结所有线程,这可能会消除多线程带来的任何优势。

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top