实体沟通如何工作?


115

我有两个用户案例:

  1. 如何将entity_A发送take-damage消息entity_B
  2. 如何entity_A查询entity_B的HP?

到目前为止,这是我遇到的情况:

  • 消息队列
    1. entity_A创建一条take-damage消息并将其发布到entity_B的消息队列中。
    2. entity_A创建一条query-hp消息并将其发布到entity_Bentity_B作为回报,创建一条response-hp消息并将其发布到entity_A
  • 发布/订阅
    1. entity_B订阅take-damage消息(可能具有一些抢先过滤功能,因此仅传递相关消息)。 entity_A产生take-damage引用消息entity_B
    2. entity_A订阅update-hp消息(可能已过滤)。每个帧都entity_B广播update-hp消息。
  • 信号/插槽
    1. ???
    2. entity_Aupdate-hp插槽连接到entity_Bupdate-hp信号。

有更好的东西吗?我对这些通信方案如何与游戏引擎的实体系统联系起来有正确的理解吗?

Answers:


67

好问题!在回答您提出的特定问题之前,我会说:不要低估简单性的力量。Tenpn是正确的。请记住,您要使用这些方法进行的所有操作都是找到一种优雅的方法来推迟函数调用或将调用者与被调用者分离。我可以推荐协程作为一种令人惊讶的直观方式来缓解其中的一些问题,但这有点题外话了。有时,您最好只调用该函数并接受实体A直接耦合到实体B的事实。请参见YAGNI。

就是说,我使用信号/插槽模型并结合了简单的消息传递并对此感到满意。我在C ++和Lua中使用了它,取得了相当成功的iPhone称号,而且时间表很紧。

对于信号/插槽情况,如果我希望实体A响应实体B所做的某事(例如,当某件事物死亡时解锁门),那么我可能会让实体A直接订阅实体B的死亡事件。或者,实体A可能会订阅一组实体中的每个实体,在每个触发的事件上增加一个计数器,并在N个实体死亡后解锁门。同样,“实体组”和“它们中的N个”通常是设计者在级别数据中定义的。(顺便说一句,这是协程可以真正发光的一个区域,例如,WaitForMultiple(“ Dying”,entA,entB,entC); door.Unlock();)

但是当涉及到与C ++代码紧密相关的反应或固有的短暂游戏事件时,这可能变得很麻烦:造成损害,重新装填武器,调试,玩家驱动的基于位置的AI反馈。这是消息传递可以填补空白的地方。从本质上讲,它可以归结为“告诉该区域中的所有实体在3秒钟内造成伤害”或“每当完成物理过程以弄清楚我是谁射击的,告诉他们运行此脚本功能”。很难弄清楚如何使用发布/订阅或信号/插槽很好地做到这一点。

这很容易被矫kill过正(与tenpn的示例相反)。如果您采取很多措施,它也可能导致效率低下。尽管存在缺点,但这种“消息和事件”方法与脚本游戏代码(例如在Lua中)很好地结合在一起。脚本代码可以定义自己的消息和事件并对其做出反应,而完全不需要C ++代码。脚本代码可以轻松发送触发C ++代码的消息,例如更改级别,播放声音,甚至只是让武器设置TakeDamage消息所造成的损害。它节省了我很多时间,因为我不必不断地与luabind混蛋。而且它使我所有的luabind代码都集中在一个地方,因为没有太多。正确耦合后

同样,我对用例2的经验是,最好将它作为另一个方向处理。当健康发生重大变化时,请触发事件/发送消息,而不是询问实体的健康状况。

在接口方面,顺便说一句,我最终得到了三个类来实现所有这些:EventHost,EventClient和MessageClient。EventHost创建插槽,EventClient订阅/连接插槽,MessageClient将委托与消息关联。请注意,MessageClient的委托目标不一定是拥有关联的同一对象。换句话说,MessageClients可以单独存在以将消息转发到其他对象。FWIW,主机/客户端的比喻有点不合适。源/汇可能是更好的概念。

抱歉,我有点在那儿乱逛。这是我的第一个答案:)我希望这是有道理的。


感谢您的回答。真知灼见。我过度设计消息传递的原因是因为Lua。我希望能够在没有新的C ++代码的情况下创建新武器。因此,您的想法回答了我一些未提出的问题。
deft_code 2010年

至于协程,我也是协程的忠实信徒,但我从来没有在C ++中使用过它们。我对在lua代码中使用协程来处理阻塞调用(例如等待死亡)的含糊希望。值得付出努力吗?恐怕我对C ++中的协程的强烈渴望可能会蒙蔽我的视线。
deft_code 2010年

最后,什么是iPhone游戏?我可以获取有关您使用的实体系统的更多信息吗?
deft_code 2010年

2
实体系统主要是C ++。因此,例如有一个处理Imp行为的Imp类。Lua可以在生成时或通过消息来更改Imp的参数。Lua的目标是安排在紧迫的时间内,调试Lua代码非常耗时。我们使用Lua编写脚本级别(哪些实体去哪儿,在您按下触发器时发生的事件)。因此,在Lua中,我们会说诸如SpawnEnt(“ Imp”)之类的东西,其中Imp是手动注册的工厂关联。它始终会生成一个全局实体池。漂亮又简单。我们使用了很多 smart_ptr和weak_ptr。
布拉夫

1
因此,BananaRaffle:您能说这是您答案的准确摘要:“您发布的所有三种解决方案都有其用途,其他解决方案也有它们的用途。不要寻找一个完美的解决方案,只要在有意义的地方使用所需的内容。”
Ipsquiggle 2010年

76
// in entity_a's code:
entity_b->takeDamage();

您问商业游戏是如何做到的。;)


8
否决票?说真的,这就是通常的做法!实体系统很棒,但它们并没有帮助达到早期里程碑。
tenpn

我专业制作Flash游戏,这就是我的工作方式。您调用敌人.damage(10),然后从公共获取者那里查找所需的任何信息。
伊恩

7
认真地讲,这是商业游戏引擎如何做到的。他不是在开玩笑。通常是Target.NotifyTakeDamage(DamageType,DamageAmount,DamageDealer等)。
AA Grapsas

3
商业游戏也会拼错“损害”吗?:-P
cket

15
是的,除其他外,它们确实会误摔。:)
LearnCocos2D 2010年

17

一个更严肃的答案:

我看过黑板用了很多。简单版本只不过是用诸如实体的HP之类的东西更新的strut,然后实体可以查询。

您的黑板可以是该实体的世界视图(询问B的黑板其HP是什么),也可以是实体的世界视图(A查询其黑板以查看A的目标HP是什么)。

如果仅在框架中的同步点更新黑板,则可以在以后从任何线程读取它们,从而使多线程实现起来非常简单。

更高级的黑板可能更像是哈希表,将字符串映射到值。这是更易于维护的,但是显然需要运行时成本。

传统上,黑板仅是单向通信-它不会损坏碟子。


我以前从未听说过黑板模型。
deft_code

它们还有助于减少依赖性,就像事件队列或发布/订阅模型一样。
tenpn

2
这也是“理想的” E / C / S系统应如何工作的规范“定义”。系统是作用于其上的代码。(long long int在纯ECS系统中,实体当然是s或相似的。)
BRPocock 2011年

6

我已经研究了这个问题,并且看到了一个不错的解决方案。

基本上,所有这些都与子系统有关。它类似于Tenpn提到的黑板构想。

实体是由组件组成的,但它们只是财产袋。实体本身未实现任何行为。

假设实体具有“健康”组件和“损坏”组件。

然后,您将拥有一些MessageManager和三个子系统:ActionSystem,DamageSystem,HealthSystem。某一时刻,ActionSystem会对游戏世界进行计算并生成一个事件:

HIT, source=entity_A target=entity_B power=5

此事件已发布到MessageManager。现在,在某个时间点,MessageManager遍历了挂起的消息,并发现DamageSystem已订阅了HIT消息。现在,MessageManager将HIT消息传递到DamageSystem。DamageSystem遍历具有损坏组件的实体列表,根据命中力或两个实体的其他状态等计算损坏点,然后发布事件

DAMAGE, source=entity_A target=entity_B amount=7

HealthSystem已订阅了DAMAGE消息,现在,当MessageManager将DAMAGE消息发布给HealthSystem时,HealthSystem可以访问带有其健康组件的实体entity_A和Entity_B,因此HealthSystem可以再次执行其计算(并可能发布相应的事件)到MessageManager)。

在这样的游戏引擎中,消息的格式是所有组件和子系统之间的唯一耦合。子系统和实体是完全独立的,彼此之间不了解。

我不知道某个真正的游戏引擎是否已经实现了这个想法,但是它看起来非常坚实和干净,我希望有一天自己为我的爱好者级别的游戏引擎实现它。


这比IMO接受的答案要好得多。解耦,可维护和可扩展(并且也不像的笑话那样带来耦合灾难entity_b->takeDamage();
Danny Yaroslavski

4

为什么没有全局消息队列,例如:

messageQueue.push_back(shared_ptr<Event>(new DamageEvent(entityB, 10, entityA)));

带有:

DamageEvent(Entity* toDamage, uint amount, Entity* damageDealer);

在游戏循环/事件处理结束时:

while(!messageQueue.empty())
{
    Event e = messageQueue.front();
    messageQueue.pop_front();
    e.Execute();
}

我认为这是Command模式。并且Execute()是中的纯虚函数Event,它的派生定义并完成任务。所以在这里:

DamageEvent::Execute() 
{
    toDamage->takeDamage(amount); // Or of course, you could now have entityA get points, or a recognition of damage, or anything.
}

3

如果您的游戏是单人游戏,则只需使用目标对象方法(如tenpn建议)。

如果您是(或想要支持)多人游戏(确切地说是多客户端),请使用命令队列。

  • 当A对客户端1的B造成损害时,只需将损害事件排队。
  • 通过网络同步命令队列
  • 处理双方排队的命令。

2
如果您对避免作弊很认真,那么A根本不会对客户端造成伤害。拥有A的客户端向服务器发送“攻击B”命令,该命令完全执行tenpn所说的;然后,服务器将该状态与所有相关客户端同步。

@Joe:是的,如果有一个服务器是值得考虑的有效点,但是有时可以信任客户端(例如在控制台上)以避免繁重的服务器负载。
安德烈亚斯(Andreas)2010年

2

我会说:两者都不用,只要您不明确需要损害的即时反馈即可。

承担损害的实体/组件/无论采取什么措施,都应将事件推送到本地事件队列或保存损害事件的同等级别的系统。

然后应该有一个覆盖系统,可以访问两个实体,并从实体a请求事件并将其传递给实体b。通过不创建任何事件都可以随时随地将事件传递给任何事物的通用事件系统,您可以创建显式的数据流,从而始终使代码更易于调试,更容易衡量性能,更易于理解和阅读,并且经常通常会导致设计更完善的系统。


1

只需拨打电话即可。不要执行query-hp追随的request-hp -如果遵循该模型,您将遭受很大的伤害。

您可能还想看看Mono Continuations。我认为这对于NPC是理想的选择。


1

那么,如果我们让玩家A和B试图在相同的update()周期中互相打击怎么办?假设玩家A的Update()恰好发生在第1周期的玩家B的Update()之前(或勾选,或您称之为的任何东西)。我可以想到两种情况:

  1. 通过消息立即处理:

    • 玩家A.Update()看到玩家想要击中B,玩家B收到一条消息,通知其损坏。
    • 玩家B.HandleMessage()更新了玩家B的生命值(他去世了)
    • 玩家B.Update()看到玩家B已死..他无法攻击玩家A

这是不公平的,玩家A和B应该互相击中,玩家B在击中A之前就死了,只是因为那个实体/游戏对象稍后获得了update()。

  1. 排队留言

    • 玩家A.Update()看到玩家想要击中B,玩家B收到一条消息,通知其损坏并将其存储在队列中
    • 播放器A.Update()检查其队列,该队列为空
    • 玩家B.Update()首先检查移动,因此玩家B也向玩家A发送了一条消息,同时也有损坏
    • 播放器B.Update()还处理队列中的消息,处理播放器A造成的损坏
    • 新周期(2):玩家A要喝一种健康药水,因此调用了玩家A.Update()并处理了移动
    • 播放器A.Update()检查消息队列并处理播放器B造成的损坏

再次,这是不公平的。.玩家A应该在同一回合/周期/滴答中获得生命值!


4
您并未真正回答这个问题,但我认为您的回答本身将是一个很好的问题。为什么不继续问问如何解决这种“不公平”的优先次序呢?
bummzack 2011年

我怀疑大多数游戏是否会关注这种不公平性,因为它们经常更新,因此很少出现问题。一种简单的解决方法是在更新时在实体列表中前后迭代之间进行切换。
Kylotan

我使用了2次调用,所以我对所有实体调用Update(),然后在循环之后再次迭代并调用pEntity->Flush( pMessages );。当entity_A产生一个新事件时,entity_B不会在该帧中读取它(它也有机会吸收药水)然后都受到伤害,然后它们处理药水恢复的消息,这将是队列中的最后一条消息。无论如何,玩家B仍然死亡,因为药水消息是队列:P中的最后一条消息,但它对于其他类型的消息(如清除指向死实体的指针)可能很有用。
Pablo Ariel

我认为在框架级别,大多数游戏实现都是不公平的。就像Kylotan所说的。
v.oddou 2014年

这个问题非常容易解决。只需在消息处理程序或其他任何处理程序中对彼此施加损害。您绝对不应该在消息处理程序内部将播放器标记为已死。在“ Update()”中,您只需执行“ if(hp <= 0)die();”。(例如,在“ Update()”的开头)。这样,双方就可以同时杀死对方。另外:通常,您不会直接伤害玩家,而是会通过子弹之类的中间物体伤害玩家。
塔拉
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.