我正在Java平台上开发一个实时战略游戏克隆,我有一些关于放置位置和如何管理游戏状态的概念性问题。游戏使用Swing / Java2D作为渲染。在当前的开发阶段,不存在模拟,也不存在AI,只有用户能够改变游戏的状态(例如,建造/拆除建筑物,添加 - 移除生产线,组装车队和设备)。因此,可以在事件调度线程中执行游戏状态操作而无需任何渲染查找。游戏状态还用于向用户显示各种聚合信息。
然而,由于我需要引入模拟(例如,构建进度,人口变化,车队运动,制造过程等),在Timer和EDT中更改游戏状态肯定会减慢渲染速度。
假设模拟/ AI操作每500毫秒执行一次,我使用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决策进行更改。还确保,在前一个步骤写回所有更改之前,不会启动新的模拟/ AI步骤。
我的目标是:
选项:
所有这些都有模型和游戏的优点,缺点和原因。
更新2:我在谈论this游戏。我的克隆是here。屏幕截图可能有助于想象渲染和数据模型的交互。
更新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种这种重叠的情况。
答案 0 :(得分:3)
这听起来好像可以从客户端/服务器方法中受益:
播放器是客户端 - 交互性和渲染就在这一端发生。所以玩家按下按钮,请求进入服务器。服务器的回复返回,播放器的状态更新。在这些事情发生之间的任何一点,屏幕都可以重新绘制,它反映了客户目前所知的游戏状态。
AI同样是一个客户端 - 它相当于一个机器人。
模拟是服务器。它在不同时间从其客户端获取更新并更新世界状态,然后根据需要将这些更新发送给每个人。这就是它与你的情况相关的地方:模拟/ AI需要一个静态的世界,许多事情同时发生。服务器可以简单地将更改请求排队并在将更新发送回客户端之前应用它们。因此,就服务器而言,游戏世界实际上并没有实时变化,只要服务器很好地决定它,它就会发生变化。
最后,在客户端,您可以通过执行一些快速近似计算并显示结果(因此满足即时需求)来防止按下按钮和查看结果之间的延迟,然后在显示更正确的结果时服务器到处跟你说话。
请注意,这实际上并不一定要通过互联网的TCP / IP方式实现,只是有助于在这些方面考虑它。
或者,您可以将模拟期间保持数据一致性的责任放在数据库上,因为它们已经在构建时考虑了锁定和一致性。像sqlite这样的东西可以作为非网络解决方案的一部分。
答案 1 :(得分:0)
我不确定我是否完全理解您正在寻找的行为,但听起来您需要类似状态更改线程/队列的内容,因此所有状态更改都由单个线程处理。
为状态更改队列创建一个api,可能类似于SwingUtilities.invokeLater()和/或SwingUtilities.invokeAndWait()来处理状态更改请求。
如何反映在我认为的gui取决于你正在寻找的行为。即,因为当前状态为0美元而无法提取资金,或者在处理提款请求时向用户回弹该帐户为空。 (可能不是那个术语;-))
答案 2 :(得分:0)
最简单的方法是使模拟足够快以在EDT中运行。喜欢有效的程序!
对于双线程模型,我建议将域模型与渲染模型同步。渲染模型应该保留来自域模型的数据。
更新:在模拟线程中锁定渲染模型。遍历渲染模型更新,其中与预期的内容不同,更新渲染模型。完成遍历后,解锁渲染模型并安排重新绘制。请注意,在这种方法中,您不需要大量的听众。
渲染模型可以具有不同的深度。在一个极端,它可能是一个图像,更新操作只是用新的图像对象替换单个引用(这不会处理,例如,调整大小或其他非常好的表面交互)。你可能不会费心检查一个项目是否有变化,只是更新eveything。
答案 3 :(得分:0)
如果更改游戏状态很快(一旦你知道要改变什么)你可以像其他Swing模型一样对待游戏状态,只改变或查看EDT中的状态。如果改变游戏状态不是很快,那么你可以同步状态变化并在swing worker / timer(但不是EDT)中进行,或者你可以在与EDT类似的单独线程中进行(此时你看看使用BlockingQueue
来处理变更请求)。如果UI从不必从游戏状态检索信息,而是通过侦听器或观察者发送渲染更改,则最后一个更有用。
答案 4 :(得分:0)
是否可以逐步更新游戏状态并且仍然具有一致的模型?例如,在渲染/用户更新之间重新计算行星/玩家/舰队对象的子集。
如果是这样,您可以在EDT中运行增量更新,只有在允许EDT处理用户输入和渲染之前才计算状态的一小部分。
在EDT中的每次增量更新之后,您需要记住有多少模型需要更新,并在EDT上安排新的SwingWorker,以便在执行任何待处理的用户输入和渲染后继续此处理。
这样可以避免复制或锁定游戏模型,同时仍然保持用户交互响应。
答案 5 :(得分:0)
我认为你不应该让World存储任何数据或对任何对象本身进行更改,它应该仅用于维护对象的引用,并且当该对象需要更改时,让Player进行更改更改它直接。在这种情况下,您唯一需要做的就是同步游戏世界中的每个对象,这样当玩家进行更改时,没有其他玩家可以这样做。以下是我在想的一个例子:
玩家A需要了解一个星球,所以它向世界询问该星球(如何依赖于你的实施)。 World返回玩家A要求的Planet对象的引用。玩家A决定进行更改,因此它会这样做。让我们说它增加了一个建筑物。向Planet添加建筑物的方法是同步的,因此一次只有一个玩家可以这样做。建筑物将跟踪其自己的施工时间(如果有的话),因此Planet的添加建筑方法几乎可以立即释放。这样,多个玩家可以同时询问同一行星上的信息而不会相互影响,玩家可以几乎同时添加建筑物,而不会出现滞后现象。如果两个玩家正在寻找放置建筑物的地方(如果这是游戏的一部分),那么检查一个位置的适用性将是一个查询而不是一个改变。
如果这不能回答你的问题,我很抱歉,我不确定我是否理解正确。
答案 6 :(得分:0)
如何实现管道和过滤器架构。如果过滤器不够快,管道将过滤器连接在一起并排队请求。处理在过滤器内部发生第一个过滤器是AI引擎,而渲染引擎是由一组后续过滤器实现的。
在每个计时器滴答声中,新动态世界状态的计算基于所有输入(时间也是输入)和插入第一个管道的副本。
在最简单的情况下,渲染引擎实现为单个过滤器。它只从输入管道获取状态快照并将其与静态状态一起呈现。在实时游戏中,如果管道中存在多个状态,渲染引擎可能想要跳过状态,而如果您正在进行基准测试或输出您想要渲染每个视频的视频。
您可以将渲染引擎分解的过滤器越多,并行性就越好。也许甚至可以分解AI引擎,例如您可能希望将动态状态分为快速变化和缓慢变化状态。
这种架构为您提供了良好的并行性,而无需大量同步。
这种架构的一个问题是垃圾收集每次都会经常冻结所有线程,可能会破坏从多线程中获得的任何优势。
答案 7 :(得分:0)
看起来你需要一个优先级来对模型进行更新,其中用户的更新优先于模拟和其他输入的更新。我听到你说的是,用户总是需要立即反馈他的行为,而其他输入(模拟,否则)可能需要工作时间超过一个模拟步骤。 然后在priorityqueue上同步。