面对EDT,如何管理游戏状态?
我正在 Java 平台上开发一个实时策略游戏克隆,我有一些关于放置在哪里以及如何管理游戏状态的概念性问题。 游戏使用Swing/Java2D作为渲染。 在当前的开发阶段,没有模拟,没有人工智能,只有用户能够改变游戏的状态(例如,建造/拆除建筑物、添加/删除生产线、组装车队和设备)。 因此,游戏状态操作可以在事件分派线程中执行,而无需任何渲染查找。 游戏状态还用于向用户显示各种聚合信息。
然而,由于我需要引入模拟(例如,建筑进度、人口变化、舰队移动、制造过程等),因此在计时器和 EDT 中更改游戏状态肯定会减慢渲染速度。
假设每 500 毫秒执行一次模拟/AI 操作,我使用 SwingWorker 进行大约 250 毫秒长度的计算。 我如何确保模拟和可能的用户交互之间不存在关于游戏状态读取的竞争条件?
我知道模拟结果(少量数据)可以通过 SwingUtilities.invokeLater() 调用有效地移回 EDT。
游戏状态模型似乎太复杂了,无法在任何地方使用不可变的值类。
有没有相对正确的方法来消除这种读竞争条件? 也许在每个计时器滴答声上进行完整/部分游戏状态克隆,或者将游戏状态的生存空间从 EDT 更改为其他线程?
更新:(来自我给出的评论) 该游戏由 13 名人工智能控制的玩家和 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()和distributePower()之间。 现在想象一下这样的情况:游戏模型的各个部分之间有 50 个此类重叠。
I'm developing a real time strategy game clone on the Java platform and I have some conceptional questions about where to put and how to manage the game state. The game uses Swing/Java2D as rendering. In the current development phase, no simulation and no AI is present and only the user is able to change the state of the game (for example, build/demolish a building, add-remove production lines, assemble fleets and equipment). Therefore, the game state manipulation can be performed in the event dispatch thread without any rendering lookup. The game state is also used to display various aggregated information to the user.
However, as I need to introduce simulation (for example, building progress, population changes, fleet movements, manufacturing process, etc.), changing the game state in a Timer and EDT will surely slow down the rendering.
Lets say the simulation/AI operation is performed in every 500ms and I use SwingWorker for the computation of about 250ms in length. How can I ensure, that there is no race condition regarding the game state reads between the simulation and the possible user interaction?
I know that the result of the simulation (which is small amount of data) can be efficiently moved back to the EDT via the SwingUtilities.invokeLater() call.
The game state model seems to be too complex to be infeasible for just using immutable value classes everywhere.
Is there a relatively correct approach to eliminate this read race condition? Perhaps doing a full/partial game state cloning on every timer tick or change the living space of the game state from EDT into some other thread?
Update: (from the comments I gave)
The game operates with 13 AI controlled players, 1 human player and has about 10000 game objects (planets, buildings, equipment, research, etc.). A game object for example has the following attributes:
World (Planets, Players, Fleets, ...) Planet (location, owner, population, type, map, buildings, taxation, allocation, ...) Building (location, enabled, energy, worker, health, ...)
In a scenario, the user builds a new building onto this planet. This is performed in EDT as the map and buildings collection needs to be changed. Parallel to this, a simulation is run on every 500ms to compute the energy allocation to the buildings on all game planets, which needs to traverse the buildings collection for statistics gathering. If the allocation is computed, it is submitted to the EDT and each building's energy field gets assigned.
Only human player interactions have this property, because the results of the AI computation are applied to the structures in EDT anyway.
In general, 75% of the object attributes are static and used only for rendering. The rest of it is changeable either via user interaction or simulation/AI decision. It is also ensured, that no new simulation/AI step is started until the previous one has written back all changes.
My objectives are:
- Avoid delaying the user interaction, e.g. user places the building onto the planet and only after 0.5s gets the visual feedback
- Avoid blocking the EDT with computation, lock wait, etc.
- Avoid concurrency issues with collection traversal and modification, attribute changes
Options:
- Fine grained object locking
- Immutable collections
- Volatile fields
- Partial snapshot
All of these have advantages, disadvantages and causes to the model and the game.
Update 2: I'm talking about this game. My clone is here. The screenshots might help to imagine the rendering and data model interactions.
Update 3:
I'll try to give a small code sample for clarify my problem as it seems from the comments it is misunderstood:
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));
}
}
So the overlapping is between the onAddBuildingClicked() and distributePower(). Now imagine the case where you have 50 of these kind of overlappings between various parts of the game model.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
这听起来像是可以从客户端/服务器方法中受益:
玩家是客户端 - 交互和渲染发生在客户端。 因此,玩家按下按钮,请求就会发送到服务器。 服务器返回回复,玩家状态更新。 在这些事情发生之间的任何时刻,屏幕都可以重新绘制,它反映了客户端当前所知的游戏状态。
人工智能同样是一个客户端——它相当于一个机器人。
模拟的是服务器。 它在不同时间从客户端获取更新并更新世界状态,然后根据需要将这些更新发送给每个人。 这就是它与您的情况相关的地方:模拟/人工智能需要一个静态的世界,并且许多事情同时发生。 服务器可以简单地将更改请求排队并在将更新发送回客户端之前应用它们。 因此,就服务器而言,游戏世界实际上并没有实时变化,而是只要服务器决定它就在变化。
最后,在客户端,您可以通过执行一些快速近似计算并显示结果(因此满足即时需求)来防止按下按钮和查看结果之间的延迟,然后在服务器绕过时显示更正确的结果和你说话。
请注意,这实际上不必以 TCP/IP 互联网上的方式实现,只是有助于以这些术语来思考它。
或者,您可以将在模拟过程中保持数据一致性的责任放在数据库上,因为它们在构建时已经考虑了锁定和一致性。 像 sqlite 这样的东西可以作为非网络解决方案的一部分。
This sounds like it could benefit from a client/server approach:
The player is a client - interactivity and rendering happen on that end. So the player presses a button, the request goes to the server. The reply from the server comes back, and the player's state is updated. At any point between these things happening, the screen can be re-painted, and it reflects the state of the game as the client currently knows it.
The AI is likewise a client - it's the equivalent of a bot.
The simulation is the server. It gets updates from its clients at various times and updates the state of the world, then sends out these updates to everyone as appropriate. Here's where it ties in with your situation: The simulation/AI requires a static world, and many things are happening at once. The server can simply queue up change requests and apply them before sending the updates back to the client(s). So as far as the server's concerned, the game world isn't actually changing in real time, it's changing whenever the server darn well decides it is.
Finally, on the client side, you can prevent the delay between pressing the button and seeing a result by doing some quick approximate calculations and displaying a result (so the immediate need is met) and then displaying the more correct result when the server gets around to talking to you.
Note that this does not actually have to be implemented in a TCP/IP over-the-internet sort of way, just that it helps to think of it in those terms.
Alternately, you can place the responsibility for keeping the data coherent during the simulation on a database, as they're already built with locking and coherency in mind. Something like sqlite could work as part of a non-networked solution.
不确定我完全理解您正在寻找的行为,但听起来您需要诸如状态更改线程/队列之类的东西,以便所有状态更改都由单个线程处理。
为状态更改队列创建一个 api,例如 SwingUtilities.invokeLater() 和/或 SwingUtilities.invokeAndWait() 来处理状态更改请求。
我认为这如何反映在 GUI 中取决于您正在寻找的行为。 即无法取款,因为当前状态为 0 美元,或者在处理取款请求时向用户弹回帐户为空的信息。 (可能不使用该术语;-))
Not sure I fully understand the behavior you are looking for, but it sounds like you need something like a state change thread/queue so all state changes are handled by a single thread.
Create an api, maybe like SwingUtilities.invokeLater() and/or SwingUtilities.invokeAndWait() for your state change queue to handle your state change requests.
How that is reflected in the gui I think depends on the behavior you are looking for. i.e. Can't withdraw money because current state is $0, or pop back to the user that the account was empty when the withdraw request was processed. (probably not with that terminology ;-) )
最简单的方法是使模拟足够快以在 EDT 中运行。 更喜欢有效的程序!
对于双线程模型,我建议将领域模型与渲染模型同步。 渲染模型应该保留来自域模型的数据。
更新:在模拟线程中锁定渲染模型。 遍历渲染模型更新,其中与预期更新渲染模型的内容不同。 完成遍历后,解锁渲染模型并安排重新绘制。 请注意,在这种方法中,您不需要无数的听众。
渲染模型可以有不同的深度。 在一个极端情况下,它可能是一个图像,而更新操作只是用新的图像对象替换单个引用(例如,这不能很好地处理调整大小或其他表面交互)。 您可能不会费心检查某个项目是否已更改,而只是更新所有内容。
The easiest approach is to make the simulation fast enough to run in the EDT. Prefer programs that work!
For the two-thread model, what I suggest is synchronise the domain model with a rendering model. The render model should keep data on what came from the domain model.
For an update: In the simulation thread lock the render model. Traverse the render model updating where things are different from what is expected update the render model. When finished traversing, unlock the render model and schedule a repaint. Note that in this approach you don't need a bazillion listeners.
The render model can have different depths. At one extreme it might be an image and the update operation is just to replace a single reference with the new image object (this wont handle, for instance, resizing or other superficial interaction very well). You might not bother checking whether an item has change and just update eveything.
如果更改游戏状态的速度很快(一旦您知道将其更改为什么),您可以像其他 Swing 模型一样对待游戏状态,并且仅更改或查看 EDT 中的状态。 如果更改游戏状态并不快,那么您可以同步状态更改并在 Swing Worker/Timer(但不是 EDT)中执行此操作,或者您可以在与 EDT 类似的单独线程中执行此操作(此时您可以看看使用
BlockingQueue
来处理更改请求)。 如果 UI 不需要从游戏状态检索信息,而是通过侦听器或观察者发送渲染更改,则最后一个更有用。If changing the game state is fast (once you know what to change it to) you can treat the game state like other Swing models and only change or view the state in the EDT. If changing the game state is not fast, then you can either synchronize state change and do it in swing worker/timer (but not the EDT) or you can do it in separate thread that you treat similarly to the EDT (at which point you look at using a
BlockingQueue
to handle change requests). The last is more useful if the UI never has to retrieve information from the game state but instead has the rendering changes sent via listeners or observers.是否有可能增量更新游戏状态并仍然拥有一致的模型? 例如,在渲染/用户更新之间重新计算行星/玩家/舰队对象的子集。
如果是这样,您可以在 EDT 中运行增量更新,在允许 EDT 处理用户输入和渲染之前仅计算一小部分状态。
在 EDT 中的每次增量更新之后,您需要记住还有多少模型需要更新,并在 EDT 上安排一个新的 SwingWorker 以在执行任何挂起的用户输入和渲染后继续此处理。
这应该允许您避免复制或锁定游戏模型,同时仍然保持用户交互响应。
Is it possible to incrementally update the game state and still have a model that is consistent? For example recalculate for a subset of planet/player/fleet objects in between renders/user updates.
If so, you could run incremental updates in the EDT that only calculate a small part of the state before allowing the EDT to process user inputs and render.
Following each incremental update in the EDT you would need to remember how much of the model remains to be updated and schedule a new SwingWorker on the EDT to continue this processing after any pending user inputs and rendering has been performed.
This should allow you to avoid copying or locking the game model while still keeping the user interactions responsive.
我认为你不应该让世界存储任何数据或对任何对象本身进行更改,它应该只用于维护对对象的引用,并且当需要更改该对象时,让玩家直接更改它。 在这种情况下,您唯一需要做的就是同步游戏世界中的每个对象,以便当玩家进行更改时,其他玩家无法执行此操作。 这是我的想法的一个例子:
玩家 A 需要了解一个星球,因此它向 World 询问该星球(如何取决于您的实现)。 World 返回玩家 A 请求的 Planet 对象的引用。 玩家 A 决定做出改变,所以它就这么做了。 假设它增加了一座建筑物。 将建筑物添加到星球的方法是同步的,因此一次只有一个玩家可以这样做。 该建筑将跟踪其自身的建造时间(如果有),因此星球的添加建筑方法将几乎立即被释放。 这样,多个玩家可以同时询问同一星球上的信息,而不会互相影响,并且玩家可以几乎同时添加建筑物,而不会出现太大的延迟。 如果两个玩家正在寻找放置建筑物的地方(如果这是游戏的一部分),那么检查位置的适用性将是一个查询而不是更改。
如果这不能回答您的问题,我很抱歉,我不确定我是否理解正确。
I think you shouldn't have World store any data or make changes to any objects itself, it should only be used to maintain a reference to an object and when that object needs to be changed, have the Player making the change change it directly. In this event, the only thing you need to do is synchronize each object in the game world so that when a Player is making a change, no other Player can do so. Here's an example of what I'm thinking:
Player A needs to know about a Planet, so it asks World for that Planet (how is dependent upon your implementation). World returns a reference to the Planet object Player A asked for. Player A decides to make a change, so it does so. Let's say it adds a building. The method to add a building to the Planet is synchronized so only one player can do so at a time. The building will keep track of its own construction time (if any) so the Planet's add building method would be freed up almost immediately. This way multiple players can ask for information on the same planet at the same time without affecting each other and players can add buildings almost simultaneously without much appearance of lag. If two players are looking for a place to put the building (if that is part of your game), then checking the suitability of a location will be a query not a change.
I'm sorry if this doesn't answer you're question, I'm not sure if I understood it correctly.
如何实现管道和过滤器架构? 管道将过滤器连接在一起,并在过滤器不够快时对请求进行排队。 处理发生在过滤器内部。 第一个过滤器是 AI 引擎,而渲染引擎则由一组后续过滤器实现。
在每个计时器滴答声中,新的动态世界状态都会根据所有输入(时间也是输入)和插入第一个管道的副本来计算。
在最简单的情况下,您的渲染引擎是作为单个过滤器实现的。 它只是从输入管道获取状态快照并将其与静态状态一起渲染。 在实时游戏中,如果管道中存在多个状态,则渲染引擎可能希望跳过状态,而如果您正在进行基准测试或输出视频,则需要渲染每个状态。
您可以将渲染引擎分解为的过滤器越多,并行性就越好。 也许甚至可以分解人工智能引擎,例如您可能希望将动态状态分为快速变化和缓慢变化的状态。
这种架构为您提供了良好的并行性,而无需大量同步。
这种架构的一个问题是,垃圾收集每次都会频繁运行,冻结所有线程,可能会消除多线程带来的任何优势。
How about implementing a pipes and filters architecture. Pipes connect filters together and queue requests if the filter is not fast enough. Processing happens inside filters. The first filter is the AI engine while the rendering engine is implemented by a set of subsequent filters.
On every timer tick, the new dynamic world state is computed based on all the inputs (Time is also an input) and a copy inserted into the first pipe.
In the simplest case your rendering engine is implemented as a single filter. It just takes the state snapshots from the input pipe and renders it together with the static state. In a live game, the rendering engine may want to skip states if there are more than one in the pipe while if you're doing a benchmark or outputting a video you'll want to render every one.
The more filters you can decompose your rendering engine into, the better the parallelism will be. Maybe it is even possible to decompose the AI engine, e.g. you may want to separate dynamic state into fast changing and slow changing state.
This architecture gives you good parallelism without a lot of synchronization.
A problem with this architecture is that garbage collection is going to run frequently freezing all the threads every time, possible killing any advantage gained from multi-threading.
看起来您需要一个优先队列来放置模型的更新,其中用户的更新优先于来自模拟和其他输入的更新。 我听到你说的是,用户总是需要对其行为进行即时反馈,而其他输入(模拟,否则)可能需要比一个模拟步骤更长的工作时间。
然后在priorityqueue上同步。
It looks like you need a priorityqueue to put the updates to the model on, in which updates frmo the user have priority over the updates from the simulation and other inputs. What I hear you saying is that the user always needs immediate feedback over his actions wheras the other inputs (simulation, otherwise) could have workers that may take longer than one simulation step.
Then synchronize on the priorityqueue.