我正在实现一个具有以下内容的实体系统变体:
一个实体类是多一点的ID结合部件一起
一堆没有“组件逻辑” 的组件类,只有数据
一堆系统类(又名“子系统”,“管理器”)。这些完成所有实体逻辑处理。在大多数基本情况下,系统只是遍历它们感兴趣的实体列表并对其进行操作
甲MessageChannel类对象被所有的游戏系统共享。每个系统都可以订阅特定类型的消息以进行收听,还可以使用该频道将消息广播到其他系统
系统消息处理的初始变体是这样的:
- 依次在每个游戏系统上运行更新
如果系统对某个组件执行了某些操作,而其他系统可能对此操作感兴趣,则该系统会发送一条适当的消息(例如,系统调用
messageChannel.Broadcast(new EntityMovedMessage(entity, oldPosition, newPosition))
每当实体移动时)
订阅特定消息的每个系统都会得到其消息处理方法,称为
如果系统正在处理事件,并且事件处理逻辑要求广播另一条消息,则立即广播该消息,并调用另一条消息处理方法链
在我开始优化碰撞检测系统之前,这个变体是可以的(随着实体数量的增加,它变慢了)。首先,它只是使用简单的蛮力算法迭代每个实体对。然后,我添加了一个“空间索引”,该索引具有一个单元格网格,该网格存储特定单元格区域内的实体,因此仅允许对相邻单元格中的实体进行检查。
实体每次移动时,碰撞系统都会检查该实体是否与新位置的物体发生碰撞。如果是,则检测到冲突。并且,如果两个碰撞的实体都是“物理对象”(它们都具有RigidBody分量,并且打算彼此推开以免占据相同的空间),则专用的刚体分离系统会要求运动系统将实体移动到某个将他们分开的特定职位。这进而导致运动系统发送消息,通知已更改的实体位置。碰撞检测系统旨在做出反应,因为它需要更新其空间索引。
在某些情况下,这会引起问题,因为单元的内容(C#中的Entity对象的通用列表)在进行迭代时会被修改,从而导致迭代器抛出异常。
那么... 如何防止碰撞系统在检查碰撞时被打断?
当然,我可以添加一些“聪明” /“棘手”逻辑来确保正确地迭代单元格内容,但是我认为问题不在于碰撞系统本身(在其他系统中也存在类似问题),而是消息在系统之间传播时得到处理。我需要某种方式来确保特定的事件处理方法能够正常工作,而不会造成任何干扰。
我尝试过的
- 传入消息队列。每当某个系统广播消息时,该消息就会被添加到对该消息感兴趣的系统的消息队列中。当每帧调用系统更新时,将处理这些消息。问题是:如果系统A将消息添加到系统B的队列中,则如果系统B的更新要晚于系统A(在同一游戏框架中),则它工作良好;否则,它将导致该消息处理下一个游戏框架(某些系统不希望使用)
- 传出邮件队列。当系统处理事件时,它将广播的所有消息添加到传出消息队列中。消息无需等待系统更新被处理:在初始消息处理程序完成工作后,它们将立即得到处理。如果对消息的处理导致其他消息被广播,它们也将添加到传出队列中,因此所有消息都在同一帧中进行处理。问题:如果实体生存期系统(我用一个系统实现了实体生存期管理)创建了一个实体,它将通知某些系统A和B。当系统A处理该消息时,它将导致一系列消息,这些消息最终导致所创建的实体被破坏(例如,在与某个障碍物碰撞的位置处创建了一个项目符号实体,这会导致项目符号自毁)。在解析消息链时,系统B没有获得实体创建消息。因此,如果系统B也对实体破坏消息感兴趣,它将得到它,并且只有在“链”完成解析之后,它才会得到初始实体创建消息。这将导致销毁消息被忽略,创建消息被“接受”,
编辑-回答问题,评论:
- 碰撞系统迭代时,谁修改了单元格的内容?
当碰撞系统正在对某个实体及其邻居进行碰撞检查时,可能会检测到碰撞,并且该实体系统将发送一条消息,其他系统立即对此进行响应。对消息的反应可能导致其他消息被创建并立即处理。因此,即使先前的碰撞检查尚未完成,其他一些系统也可能会创建一条消息,表明碰撞系统随后需要立即进行处理(例如,移动了一个实体,因此碰撞系统需要更新其空间索引)。
- 您不能使用全局传出消息队列吗?
我最近尝试了一个全局队列。它引起新的问题。问题:我将储罐实体移动到墙实体(储罐由键盘控制)。然后我决定改变坦克的方向。为了将水箱和墙壁的每个框架分开,CollidingRigidBodySeparationSystem将水箱从墙壁上移开的最小可能量。分离方向应与水箱的移动方向相反(游戏图纸开始时,水箱应看起来好像从未移入墙壁)。但是方向变成了与新方向相反的方向,因此将储罐移动到与最初不同的另一侧。发生问题的原因:这是现在处理消息的方式(简化代码):
public void Update(int deltaTime)
{
m_messageQueue.Enqueue(new TimePassedMessage(deltaTime));
while (m_messageQueue.Count > 0)
{
Message message = m_messageQueue.Dequeue();
this.Broadcast(message);
}
}
private void Broadcast(Message message)
{
if (m_messageListenersByMessageType.ContainsKey(message.GetType()))
{
// NOTE: all IMessageListener objects here are systems.
List<IMessageListener> messageListeners = m_messageListenersByMessageType[message.GetType()];
foreach (IMessageListener listener in messageListeners)
{
listener.ReceiveMessage(message);
}
}
}
代码流如下(假设它不是第一个游戏框架):
- 系统开始处理TimePassedMessage
- InputHandingSystem将按键转换为实体动作(在这种情况下,向左箭头变为MoveWest动作)。实体动作存储在ActionExecutor组件中
- ActionExecutionSystem响应实体动作,在消息队列的末尾添加了MovementDirectionChangeRequestedMessage。
- MovementSystem根据Velocity组件数据移动实体位置,并将PositionChangedMessage消息添加到队列末尾。使用上一帧的移动方向/速度来完成移动(比方说北)
- 系统停止处理TimePassedMessage
- 系统开始处理MovementDirectionChangeRequestedMessage
- MovementSystem根据要求更改实体速度/运动方向
- 系统停止处理MovementDirectionChangeRequestedMessage
- 系统开始处理PositionChangedMessage
- CollisionDetectionSystem检测到由于一个实体移动,它遇到了另一个实体(坦克进入墙内)。它将CollisionOccuredMessage添加到队列
- 系统停止处理PositionChangedMessage
- 系统开始处理CollisionOccuredMessage
- CollidingRigidBodySeparationSystem通过将水箱和墙壁分开来对碰撞做出反应。由于墙是静态的,因此只有水箱可以移动。坦克的移动方向被用作指示坦克来自何处。方向相反
BUG:坦克移动该框架时,它使用的运动方向是前一个框架的运动,但是当分离时,即使它已经不同,也使用了THIS框架的运动方向。那不是它应该如何工作的!
为避免此错误,需要将旧的运动方向保存在某个地方。我可以将其添加到某个组件中只是为了修复此特定的错误,但是这种情况是否表示处理消息的根本错误的方式?分离系统为什么要关心它使用哪个运动方向?我该如何优雅地解决这个问题?
- 您可能想阅读gamadu.com/artemis,以了解他们对Aspects所做的工作,这些方面解决了您所遇到的一些问题。
实际上,我已经对Artemis熟悉了一段时间了。研究了它的源代码,阅读了论坛等等。但是我看到“方面”仅在几个地方被提及,据我所知,它们基本上是指“系统”。但是我看不到阿尔emi弥斯如何解决我的一些问题。它甚至不使用消息。
- 另请参阅:“实体通信:消息队列与发布/订阅与信号/插槽”
我已经阅读了有关实体系统的所有gamedev.stackexchange问题。这似乎并没有讨论我面临的问题。我想念什么吗?
- 以不同的方式处理这两种情况,更新网格不需要依赖运动消息,因为它是碰撞系统的一部分
我不确定你是什么意思。CollisionDetectionSystem的较早实现只在更新时检查冲突(当处理了TimePassedMessage时),但是由于性能,我不得不尽可能地减少检查。因此,当一个实体移动时(我游戏中的大多数实体都是静态的),我切换到了碰撞检查。