如何在回合制游戏中提升实体组件的游戏状态?


9

到目前为止,我所使用的实体组件系统的工作原理基本上类似于Java的artemis:

  • 组件中的所有数据
  • 无状态独立系统(至少达到在初始化时不需要输入的程度)对仅包含特定系统感兴趣的组件的每个实体进行迭代
  • 所有系统都处理它们的实体一tick,然后整个过程重新开始。

现在,我尝试将其首次应用于基于回合的游戏,在继续进行之前,必须以相对于彼此的固定顺序进行大量事件和响应。一个例子:

玩家A受到剑的伤害。对此,A的装甲可以踢进并降低受到的伤害。由于变得越来越弱,A的移动速度也会降低。

  • 所遭受的损害是引起整个互动的原因
  • 必须先计算护甲并将其应用于即将来临的伤害,然后再将伤害应用于玩家
  • 直到实际造成伤害之后,才能将移动速度降低应用于某个单位,因为它取决于最终伤害量。

事件还可以触发其他事件。使用装甲减少剑的伤害会导致剑破碎(这必须在伤害减少完成之前进行),进而会引起其他事件,这实际上是对事件的递归评估。

总而言之,这似乎导致了一些问题:

  1. 很多浪费的处理周期:大多数系统(保存总是运行的东西,例如渲染)在没有“轮到他们”工作的时候根本没有任何值得做的事情,并且花费大部分时间等待游戏进入有效的工作状态。这会给每个这样的系统带来很多麻烦,随着越来越多的状态添加到游戏中,这些检查会不断增长。
  2. 为了确定系统是否可以处理游戏中存在的实体,他们需要某种方式来监视其他不相关的实体/系统状态(负责处理损害的系统需要知道是否已使用装甲)。这要么使系统承担多重责任,要么产生对其他系统的需求,而没有其他目的,只能在每个处理周期之后扫描实体集合,并通过告诉侦听器何时可以做某件事与一组侦听器进行通信。

以上两点假设系统在同一组实体上运行,最终使用其组件中的标志更改状态。

解决此问题的另一种方法是,由于单个系统的工作而增加/删除了组件(或创建了全新的实体),从而提高了游戏状态。这意味着,每当系统实际具有匹配实体时,它都知道可以对其进行处理。

但是,这使系统负责触发后续系统,因此很难对程序行为进行推理,因为单个系统交互不会导致错误的出现。添加新系统也变得更加困难,因为无法在不知道它们如何影响其他系统的情况下无法实施它们(并且可能必须修改先前的系统以触发新系统感兴趣的状态),这有点违反了拥有单独系统的目的一个任务。

这是我必须忍受的东西吗?我所看到的每个ECS示例都是实时的,并且很容易看到这种“每游戏一个迭代”循环在这种情况下的工作方式。而且我仍然需要它来进行渲染,这似乎真的不适合每次发生某些事情时都会暂停其大部分功能的系统。

是否存在一些适合于此的将游戏状态向前移动的设计模式,还是我应该将所有逻辑移出循环而仅在需要时才触发它?


您并不是真的想轮询事件的发生。事件仅在事件发生时发生。Artemis不允许系统互相通信吗?
Sidar

可以,但是只能通过使用方法将它们耦合。
Aeris130年

Answers:


3

我的建议来自于我们使用组件系统的RPG项目的以往经验。我会说我讨厌在游戏代码中工作,因为那是意大利面条代码。所以我在这里没有提供太多答案,只是一个角度:

您所描述的处理玩家剑身伤害的逻辑……似乎应该由一个系统来负责所有这些。

某个地方有一个HandleWeaponHit()函数。它将访问玩家实体的ArmorComponent以获得相关的盔甲。它可能会访问攻击武器实体的WeaponComponent以破坏武器。计算最终伤害后,它将触摸MovementComponent,以使玩家达到减慢速度的目的。

至于浪费的处理周期... HandleWeaponHit()仅应在需要时触发(检测到剑击时)。

也许我要说的是:确定要在代码中放置一个断点,命中一个断点,然后逐步遍历应该在发生剑击时运行的所有逻辑。换句话说,逻辑不应分散在多个系统的tick()函数中。


以这种方式执行此操作会使添加更多行为的hit()函数变成气球。假设有一个敌人,每当剑在其视线内击中一个目标(任何目标)时,就有一个敌人掉下来大笑。HandleWeaponHit应该真正负责触发该事件吗?
Aeris130

1
您的战斗顺序很紧密,是的,命中是触发效果的原因。并非必须将所有内容分解成小系统,让这一个系统来处理这个问题,因为它确实是您的“战斗系统”,并且可以处理...战斗...
Patrick Hughes 2013年

3

这是一个古老的问题,但是现在在学习ECS时,我的自制游戏也面临着同样的麻烦,因此有些不可思议。希望它将最终进行讨论或至少发表一些评论。

我不确定它是否违反ECS概念,但是如果出现以下情况,该怎么办:

  • 添加一个EventBus以使系统发布/订阅事件对象(实际上是纯数据,但我猜不是组件)
  • 为每个中间状态创建组件

例:

  • UserInputSystem使用[DamageDealerEntity,DamageReceiverEntity,技能/武器使用的信息]触发攻击事件
  • CombatSystem已预订,并为DamageReceiver计算逃避几率。如果逃避失败,则使用相同的参数触发损害事件
  • DamageSystem订阅了此类事件并因此被触发
  • DamageSystem使用Strength,BaseWeapon伤害,伤害的类型等并将其写入具有[DamageDealerEntity,FinalOutgoingDamage,DamageType]的新IncomingDamageComponent中,并将其附加到伤害接收者实体/实体上
  • DamageSystem发射了传出的伤害
  • ArmorSystem由它触发,拾取接收方实体或通过实体中的IncomingDamage方面进行搜索以拾取IncomingDamageComponent(对于散布的多个攻击而言,最后一个可能会更好)并计算对其施加的装甲和伤害。(可选)触发碎剑事件
  • ArmorSystems会移除每个实体中的IncomingDamageComponent并将其替换为DamageReceivedComponent,并使用最终计算出的数字来影响伤口的HP和减慢速度
  • ArmorSystems发送一个IncomingDamageCalculated事件
  • 订阅了速度系统并重新计算速度
  • 已订阅HealthSystem并降低实际HP
  • 等等
  • 清理一下

优点:

  • 系统互相触发,为复杂的链事件提供中间数据
  • 通过EventBus解耦

缺点:

  • 我觉得我混合了两种方法:在事件参数中和在临时组件中。这可能是一个薄弱的地方。从理论上讲,为了使事情保持一致,我可以只触发没有数据的枚举事件,以便系统可以按方面在Entity组件中找到隐含的参数...虽然不确定是否可以
  • 不知道如何知道所有潜在感兴趣的系统是否已处理IncomingDamageCalculated,以便可以将其清除并进行下一轮处理。也许可以在CombatSystem中进行某种检查...

2

与Yakovlev的解决方案类似,我最终提出了解决方案。

基本上,我最终使用了事件系统,因为我发现转弯遵循其逻辑非常直观。该系统最终负责遵守基于回合制逻辑的游戏内单元(玩家,怪物和与之互动的任何事物),实时任务(例如渲染和输入轮询)放置在其他位置。

系统实现一个onEvent方法,该方法将一个事件和一个实体作为输入,以信号通知该实体已接收到该事件。每个系统还通过一组特定的组件订阅事件和实体。系统可用的唯一交互点是实体管理器单例,用于将事件发送到实体并从特定实体中检索组件。

当实体管理器收到一个事件,并将其发送给该实体时,它将事件置于队列的后面。当队列中有事件时,最重要的事件将被检索并发送到订阅该事件并且对接收该事件的实体的组件集感兴趣的每个系统。这些系统可以依次处理实体的组件,以及向管理器发送其他事件。

示例:玩家受到伤害,因此向玩家实体发送了伤害事件。DamageSystem订阅带有运行状况组件的发送到任何实体的损坏事件,并具有onEvent(entity,event)方法,该方法通过事件中指定的数量减少实体组件中的运行状况。

这使插入装甲系统变得容易,该装甲系统订阅发送给具有装甲组件的实体的损坏事件。它的onEvent方法通过组件中的装甲量来减少事件中的损坏。这意味着指定系统接收事件的顺序会影响游戏逻辑,因为装甲系统必须在伤害系统之前处理伤害事件才能正常工作。

但是,有时系统必须跨出接收实体。为了继续我对Eric Undersander的回应,添加一个访问游戏地图并在受到损坏的实体的x个空间内查找带有FallsDownLaughingComponent的实体,然后向其发送FallDownLaughingEvent的系统很简单。必须安排此系统在损坏系统之后接收事件,如果此时尚未取消损坏事件,则造成损坏。

出现的一个问题是,鉴于某些响应可能会产生其他响应,如何确保响应事件的发送顺序得到处理。例:

玩家移动,提示将移动事件发送到玩家实体并由移动系统接听。

排队中:运动

如果允许移动,则系统会调整玩家位置。如果不是(玩家尝试进入障碍物),它将事件标记为已取消,从而导致实体管理器丢弃该事件,而不是将其发送到后续系统。在该事件感兴趣的系统列表的最后是TurnFinishedSystem,它确认玩家已花掉自己的回合移动角色,并且他/她的回合现在结束了。这导致将TurnOver事件发送到玩家实体并放置在队列中。

排队中:营业额

现在说玩家踩到陷阱,造成伤害。TrapSystem在TurnFinishedSystem之前获取移动消息,因此首先发送损坏事件。现在,队列看起来像这样:

排队:损坏,周转

到目前为止一切都很好,将处理损坏事件,然后转弯结束。但是,如果发送其他事件作为对损坏的响应怎么办?现在,事件队列如下所示:

排队中:损坏,营业额,ResponseToDamage

换句话说,转弯将在处理任何损坏响应之前结束。

为了解决这个问题,我最终使用了两种发送事件的方法:send(事件,实体)和response(event,eventToRespondTo,实体)。

每个事件都会在响应链中保留以前的事件的记录,并且每当使用response()方法时,要响应的事件(及其响应链中的每个事件)都会在该事件的链结顶部终止。回应。初始运动事件没有这样的事件。随后的损坏响应在其列表中包含移动事件。

最重要的是,可变长度数组用于包含多个事件队列。每当管理器收到事件时,该事件就会添加到与响应链中的事件数量匹配的数组索引处的队列中。因此,初始移动事件将在[0]处添加到队列中,并且损坏和周转事件在[1]处将添加到单独的队列中,因为它们都是作为对移动的响应发送的。

发送对损坏事件的响应时,这些事件将同时包含损坏事件本身和移动,并将它们排入索引[2]的队列中。只要索引[n]在队列中有事件,这些事件将在移至[n-1]之前得到处理。这给出了以下处理顺序:

移动->伤害[1]-> ResponseToDamage [2]-> [2]为空->营业额[1]-> [1]为空-> [0]为空

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.