在实体组件系统中实现有限状态机

6
我想使用有限状态机来处理游戏中的实体状态,特别是针对“Player”实体。我的“Player”将具有诸如怠速、奔跑、跳跃、下落等状态,并需要一种管理这些状态及其之间转换的方式。在面向对象编程环境中,最简单的解决方案是使每个状态成为自己的类,并且有一个名为“handleInput”的方法来接收输入并确定是否应进行状态更改。例如,在“IdleState”中,如果发生move_right或move_left,则状态将更改为新的“RunningState”。这很容易理解和操作,因为状态的行为应该封装在状态中。
但是,在使用实体组件系统中使用FSM时,所有事物都会发生变化。状态不再是对象(因为这与组件系统的灵活性相违背),而是由不同的组件构成的不同排列。例如,“JumpState”可能具有“JumpComponent”、“AirbornMovementComponent”等组件,而“AttackState”可能具有表示攻击的组件,例如“SwingComponent”、“DamageComponent”、“SwordComponent”等。通过重新排列组件,可以创建新状态。系统的工作仅是分别处理这些组件,因为系统不关心状态,它们只关心各个组件。实际的FSM位于由实体持有的“FSMComponent”中。
这很有道理,但涉及到处理状态转换时就有些麻烦了。现在我有一个“InputSystem”,寻找具有“InputComponent”和“FSMComponent”的实体,并尝试根据当前输入更新FSM的状态。然而,这种方法并不太好。
在我看来,FSM处理输入的最佳方式是让每个状态确定它想要如何处理输入以及如何基于该输入过渡到新状态。这回到了OOP实现FSM的方式,违反了ECS的设计,其中组件仅是数据包,并且系统执行所有逻辑。在ECS中,想法是让系统处理状态转换,但这变得复杂,因为每个FSM可能具有不同的条件来在状态之间转换。
不能简单地在“InputSystem”中声明“如果输入为向右移动,则将状态设置为奔跑”。这将是针对玩家特定的,但可能不适用于所有实体。如果有一天我决定使敌人可控制,那么对于Player有效的输入可能不适用于Enemy

我的问题: 在ECS中,如何让我的有限状态机(FSM)足够通用和灵活,以允许各种状态转换的实现,而无需在系统本身中进行显式的if/else检查?

如果我方法完全错误,那有更好的解决方案来在实体组件系统中实现FSM吗?


Ash框架有一种非常有趣的方法,作者在这里写了一篇文章:http://www.richardlord.net/blog/finite-state-machines-with-ash。他还在这里提供源代码:https://github.com/richardlord/Ash/tree/master/src/ash/fsm。 - fallaciousreasoning
我遇到了一个类似的问题。我需要一个有限状态机来管理我的玩家状态,因为他们要在不同的菜单和模式之间进行导航。我认为我的方法会很简单,因为它能够满足我的需求——一个定义状态和转换的FSM组件,任何感兴趣的系统都只需要检查它,以确定是否需要执行任何操作。大部分时间,系统会查看它,然后什么也不做。这可能有些浪费,但对我来说足够简单和好用。目前而言。我真的很想知道你是怎么解决这个问题的。对于我的需求来说,切换组件开/关状态太重了。 - InfinitiesLoop
1
@InfinitiesLoop 我最终没有提取出有限状态机代码,但这是我的项目。状态机代码位于“fsm”包下(在core/src/com/cpubrew/中)。要了解如何使用它,可以在“factory”包内的“EntityFactory”中找到很多好的示例(游戏中的所有实体都在此构建)。如果你想更深入地了解它的工作原理,我很乐意讲解。 - Scooter
2个回答

5
只是在@fallaciousreasoning的帖子(以及其后续评论)上思考。
Ash实际上有两个FSM,它们在不同的级别上运行。
首先,在实体级别上运行的FSM通过在从一个状态转换到另一个状态时更改实体上组件的组成来管理(实体)状态转换。
其次,在引擎级别上运行的FSM通过更改引擎执行的系统的组成来管理(引擎)状态转换。
结合起来,它们构成了一个相当强大的FSM。
关键是确定您需要处理哪种类型的转换;一种是“数据”组成驱动转换,另一种是“逻辑”组成驱动转换。
因此,掌握了这些新的知识,让我们思考一下它是如何工作的。
在更加天真的改变组件组成的方法中,我们将使用许多“标签”组件,以及一些冗长的开关语句(或if/else检查)来处理这些实体状态的更改,最终导致过度膨胀的系统。 Ash的Entity FSM通过将给定的组件配置映射到标识符并提供可以用于触发状态转换的管理器,从而使其更加精细,并避免了那些冗长的开关语句(或if/else子句)。此管理器实例可以作为组件的属性传递,也可以作为系统的成员进行组合/注入。
或者,采用引擎FSM方法,我们将每个特定状态逻辑的每个位都分解为自己的系统,并根据给定的(引擎)状态交换它们。这种方法并非没有缺点,因为系统的缺失将影响与其关联的所有实体。但是,将系统专用于单个实体实例(例如玩家角色)并不罕见,因此在正确的上下文中,这可能会证明是有用的。想想看,通过系统交换全局影响实体可能也是可取的。
(注意:如果您的逻辑修改范围需要更窄,您可以将其限制在System中,而不涉及Engine FMS。这可以通过在System中实现State模式来实现,其中系统可以通过委托到不同的子系统来改变其行为基于状态。)
我看过一些ECS框架将System组合标记为“Phases”,但它们在本质上作为FSMs运行,其中ECS引擎使用与给定阶段相关联的不同系统集处理实体。
总之,数据组成只是方程式的一半;在尝试在ECS中实现FSM时,如何组成您的逻辑块(或块)也同样重要。

0

ash框架采用了一种非常有趣的方法,作者在这里写到了它。

他采用了与您建议的类似的方法,认为实体的状态取决于构成它的组件(这反过来又确定了哪些系统处理实体)。

为了使状态转换更容易管理,他引入了一个FiniteStateMachine类,跟踪实体处于各种状态所需的组件。

例如,如果敌人AI具有Transform和Patrol组件,则可能处于巡逻状态,因此他会将该信息注册到FiniteStateMachine中。

 var fsm = new FiniteStateMachine(enemyEntity);

 fsm.CreateState("patrol")
    .WithComponent(new Transform())
    .WithComponent(new Patrol());

当有限状态机被告知要改变实体的状态时,它会删除和添加组件以达到所需的状态。任何需要改变实体状态的系统只需要有限状态机的引用(类似于fsm.ChangeState("patrol"))。
他还公开了其源代码,您可以在此处找到(这也比我解释得更好),并且在基本的小行星游戏此处中有一个实际的示例。代码是ActionScript(我认为),但是您应该能够轻松破译它。

这个不太好用。让我解释一下。如果您决定在移动左和移动右的输入被使用时使玩家奔跑,那么您可以轮询输入并根据这些值更改状态。现在,您决定要有一个状态,其中玩家被冻结,无法移动。突然间,您的FSM出现了问题,因为现在您可以在不应奔跑的状态下奔跑。转换是特定于状态的,不应在系统范围内定义。 - Scooter
在这种情况下,运行组件不会被移除吗?这将停止实体被RunSystem处理,从而使其基本上被冻结?我正在研究这个问题,因为我已经开始开发自己的ECS系统,所以我离专家还很远。 - fallaciousreasoning
我创建了一个更加强大的系统,允许您定义转换。然后我有转换系统来处理所有具有该转换状态的实体,如果满足转换要求,则实体会改变状态。 - Scooter
1
@FaTalCubez,你介意分享一下你的实现吗?我很想看看你在代码方面是如何处理的。 - TheAddonDepot
@FaTalCubez 我也很感兴趣看到它。 - fallaciousreasoning
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接