未知的设计模式:Web 的实体组件系统 ECS

2025-06-10

未知的设计模式

实体组件系统

适用于 Web 的 ECS

如果你对软件感兴趣,你可能听说过设计模式。MVC、装饰器、工厂等等。但有一种非常棒的设计模式,非常适合建模复杂的行为,但在游戏行业之外却鲜为人知。

我们开发了一款名为 Tau Station 的叙事科幻 MMORPG(免费畅玩;快来体验吧!)。和许多开发者一样,我们很快就遇到了开发复杂物品的难题。起初,我们只想创建一个Item类并继承它。

但是如果你想制造一套军用级作战服呢?没错,它能起到盔甲的作用。但它可以反击,所以它也可以作为武器。它还能包扎轻微的伤口,所以它也可以作为医疗包。

钛星站战斗服

您是否想过游戏如何处理具有多种行为的复杂对象?许多较早的游戏文章都推荐使用多重继承,尽管众所周知存在诸多问题。如今,游戏经常使用实体组件系统 (ECS)。它功能强大,您经常会发现,创建一个新的复杂类只需在数据库中添加几个条目即可。

实体组件系统

ECS 最初是为获奖游戏《神偷:暗黑计划》开发的。它是一种无需复杂编程即可引入复杂对象行为的强大方法。它包含三个部分:

  • 实体——枪支、汽车、树木等等。
  • 组件——装甲、武器、医疗或任何可能改变行为的东西
  • 系统——响应实体组件的系统

请注意,在 ECS 中,您处理的是数据,而不是对象。这一点非常重要。

现在想象一下,你正在为孩子们开发一款游戏,让他们拍摄森林里的动物。每个“实体”可能是动物、植物、陷阱,或者任何组合(捕蝇草可以既是植物又是陷阱)。核心游戏循环可能如下所示(Perl 代码示例):

my $game = My::Awesome::Game->new(%parameters);
while ( $game->is_running ) {
    $game->get_user_input;
    $game->update_state;
    $game->draw;
}
Enter fullscreen mode Exit fullscreen mode

update_state方法可能如下所示:

sub update_state($self) {
    $self->hide_from_danger;
    $self->forage_for_food;
    $self->move;
    $self->photograph_available_animals;
}
Enter fullscreen mode Exit fullscreen mode

最后,该forage_for_food方法可能如下所示:

sub forage_for_food ($self) {
    foreach my $entity ( $self->local_entities ) {
        if ( is_animal($entity) ) {
            $self->search_for_food($entity);
        }

        # we can't use "elsif" here because more than
        # one entity type might apply to the entity
        if ( is_plant($entity) ) {
            $self->react_to_sunlight($entity);
        }

        if ( is_trap($entity) ) {
            $self->trigger_trap($entity);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

实体仅仅是数据的捆绑,而辅助函数(例如is_animalis_plant和)is_trap可识别实体是否具有特定组件。

但我们是一款网页游戏。游戏的驱动力来自Tau Station居民的链接点击,而不是游戏循环。那么我们该如何处理这个问题呢?

适用于 Web 的 ECS

数据库

Tau Station 没有游戏循环,但这并不是什么大问题。相反,我们会根据需要检查行为,主要关注的是如何创建实体。为此,我们创建了一个简单的视图,只需一条 SQL 查询即可提取所有相关数据:

         SELECT me.item_id, me.name, me.mass, ...   -- global
                ar.component_armor_id,   ...        -- armor
                md.component_medical_id, ...        -- med
                wp.component_weapon_id,  ...        -- weapon
           FROM items me
LEFT OUTER JOIN component_armors   ar ON ar.item_id = me.item_id
LEFT OUTER JOIN component_medicals md ON md.item_id = me.item_id
LEFT OUTER JOIN component_weapons  wp ON wp.item_id = me.item_id
          WHERE me.slug = ?
Enter fullscreen mode Exit fullscreen mode

关于 SQL,有几点需要注意。首先,所有商品都有一个唯一的、人类可读的“slug”,我们会在 URL 中使用。例如,“战斗服”的 slug 是combat-suit。它会在 SQL 的绑定参数(问号)中使用。

这些部分表示如果未找到该行,LEFT OUTER JOIN则组成列。NULL

最后,对于许多物品来说,这条 SQL 语句可能极其低效。然而,我们所有的物品都是不可变的,这意味着我们会缓存上述查询的结果,并且玩家物品栏中包含的“物品实例”可能包含更多信息(例如伤害等级)。

代码

使用上述数据的代码相当简单。在我们的策略中,我们有一些全局属性,例如“名称”、“质量”等等。这些都保存在我们的物品表中。每个物品可能包含一个盔甲组件(component_armors表格)、一个武器组件(component_weapons表格)等等。我们有一个Item类(它应该被命名为Entity),虽然它是一个类,但它只有用于获取物品属性的只读访问器,以及用于测试组件的谓词方法:

sub is_armor  ($self) { defined $self->component_armor_id  }
sub is_weapon ($self) { defined $self->component_weapon_id }
sub is_med    ($self) { defined $self->component_med_id    }
Enter fullscreen mode Exit fullscreen mode

因为我们LEFT OUTER JOIN在代码中使用了 s ,所以如果is_armor, predicate 方法将返回 false 。这意味着,如果有人想装备盔甲,我们可以轻松地检查这一点:component_armor_idNULL

sub equip_armor($self, $item_slug) {

    # logs and throws an exception if item not found
    my $item = $self->resolve_entity($item_slug);

    if ( $item->is_armor ) {
        # equip it
    }
    else {
        # don't equip it
    }
}
Enter fullscreen mode Exit fullscreen mode

遗憾的是,ECS 在游戏行业之外并不出名,但在我于阿姆斯特丹做了一次关于 Tau Station 的演讲后,一家位于伦敦的金融公司意识到,它是建模复杂金融工具的完美方式。我见过很多代码库,ECS 都能带来巨大的优势。它秉承“组合优于继承”的理念,并且由数据驱动。这无疑是一个巨大的优势。

如果您想进一步了解 Tau Station 及其建造过程,请参阅我在阿姆斯特丹所做的演示。

此外,这篇文章的封面图片中的代码来自游戏!

请随意在评论中留下问题,我会尽力回答。

鏂囩珷鏉ユ簮锛�https://dev.to/ovid/the-unknown-design-pattern-1l64
PREV
如何在线使用比特币和加密货币购买黄金和白银?
NEXT
我为什么删除我的 IDE;以及它如何让我的生活变得更好 我删除 IDE 的 5 个理由