未知的设计模式
实体组件系统
适用于 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;
}
该update_state
方法可能如下所示:
sub update_state($self) {
$self->hide_from_danger;
$self->forage_for_food;
$self->move;
$self->photograph_available_animals;
}
最后,该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);
}
}
}
实体仅仅是数据的捆绑,而辅助函数(例如is_animal
、is_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 = ?
关于 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 }
因为我们LEFT OUTER JOIN
在代码中使用了 s ,所以如果是is_armor
, predicate 方法将返回 false 。这意味着,如果有人想装备盔甲,我们可以轻松地检查这一点:component_armor_id
NULL
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
}
}
遗憾的是,ECS 在游戏行业之外并不出名,但在我于阿姆斯特丹做了一次关于 Tau Station 的演讲后,一家位于伦敦的金融公司意识到,它是建模复杂金融工具的完美方式。我见过很多代码库,ECS 都能带来巨大的优势。它秉承“组合优于继承”的理念,并且由数据驱动。这无疑是一个巨大的优势。
如果您想进一步了解 Tau Station 及其建造过程,请参阅我在阿姆斯特丹所做的演示。
此外,这篇文章的封面图片中的代码来自游戏!
请随意在评论中留下问题,我会尽力回答。
鏂囩珷鏉ユ簮锛�https://dev.to/ovid/the-unknown-design-pattern-1l64