esper是一个专注于性能的Python轻量级实体系统(ECS)
项目描述
Esper是一个针对Python的轻量级实体系统模块,专注于性能
Esper是一个MIT许可证的实体系统,或实体组件系统(ECS)。设计基于最初由Adam Martin和其他人推广的实体系统概念。Esper的主要重点是最大化性能,同时处理大多数常见用例。
有关ECS模式更多信息,以下资源可能对你有帮助: https://github.com/SanderMertens/ecs-faq/blob/master/README.md https://github.com/jslee02/awesome-entity-component-system/blob/master/README.md https://en.wikipedia.org/wiki/Entity_component_system
API文档托管在ReadTheDocs: https://esper.readthedocs.io 由于项目规模较小,此README当前作为通用使用文档。
:warning: Esper 3.0引入了重大更改。版本3.0删除了World对象,并将其方法迁移到模块级别函数。可以创建多个上下文并在它们之间切换。v2.x README可以在以下位置找到: https://github.com/benmoran56/esper/blob/v2_maintenance/README.md
兼容性
Esper旨在针对所有当前支持的Python版本(任何非EOL的Python版本)。Esper是用100%纯Python编写的,因此 任何 合规的解释器都应该可以工作。目前对CPython和PyPy3都进行了自动测试。
安装
Esper 是一个没有依赖的纯 Python 包,因此安装非常灵活。您只需将 esper 文件夹直接复制到您的项目中,然后 import esper。您还可以通过 PyPi 的 pip
在您的 site-packages 中安装:
pip install --user --upgrade esper
或者从源目录:
pip install . --user
设计
- 世界上下文
Esper 使用“世界”上下文的概念。当您第一次 import esper
时,一个默认的上下文是活动的。您通过在 esper
模块上调用函数来创建实体、分配组件、注册处理器等。实体、组件和处理器可以在您的游戏运行时创建、分配或删除。每次游戏循环迭代只需要一个简单的 esper.process()
调用。高级用户可以切换上下文,这对于隔离具有不同处理器要求的不同的游戏场景非常有用。
- 实体
实体是简单的整数 ID(1、2、3、4 等)。实体“创建”了,但通常不直接使用。相反,它们只是简单地用作内部组件数据库中的 ID,以跟踪组件集合。创建实体是通过 esper.create_entity()
函数完成的。
- 组件
组件定义为简单的 Python 类。为了保持纯实体系统设计哲学,它们不应包含任何逻辑。它们可能有初始化代码或可能具有 Python 属性,但没有任何处理逻辑。一个简单的组件可以定义如下:
class Position:
def __init__(self, x=0.0, y=0.0):
self.x = x
self.y = y
为了节省输入,标准库的 dataclass 装饰器非常有用。 https://docs.pythonlang.cn/3/library/dataclasses.html#module-dataclasses 这个装饰器简化了定义您的组件类。不需要重复属性名称,您仍然可以使用位置或关键字参数实例化组件:
from dataclasses import dataclass as component
@component
class Position:
x: float = 0.0
y: float = 0.0
- 处理器
处理器,也常称为“系统”,是所有处理逻辑定义和执行的地方。所有处理器都必须继承自 esper.Processor 类,并有一个名为 process 的方法。除此之外,没有限制。您可以定义可能需要的任何其他方法。一个简单的处理器可能看起来像:
class MovementProcessor(esper.Processor):
def process(self):
for ent, (vel, pos) in esper.get_components(Velocity, Position):
pos.x += vel.x
pos.y += vel.y
在上面的代码中,您可以看到 esper.get_components() 函数的标准用法。此函数允许高效地迭代包含指定组件类型的所有实体。此函数可以用于一次性查询两个或多个组件。请注意,需要元组解包才能返回组件对:(vel, pos)。除了组件之外,您还会获得当前速度/位置组件对的实体 ID(ent 对象)的引用。这个实体 ID 在各种情况下都可能很有用。例如,如果您的处理器需要删除某些实体,您可以在该实体 ID 上调用 esper.delete_entity() 函数。另一个常见的用途是,如果某些条件满足,您希望在该实体上添加或删除组件。
快速入门
要开始,只需导入 esper:
import esper
从那里,定义一些组件,并创建使用它们的实体:
player = esper.create_entity()
esper.add_component(player, Velocity(x=0.9, y=1.2))
esper.add_component(player, Position(x=5, y=5))
可选地,组件实例可以直接在创建时分配给实体:
player = esper.create_entity(Velocity(x=0.9, y=1.2), Position(x=5, y=5))
设计一些在这些组件类型上操作的处理器,然后将它们注册到 Esper 以进行处理。您可以指定一个可选的优先级(数字越高越先处理)。默认情况下,所有处理器都是优先级“0”:
movement_processor = MovementProcessor()
collision_processor = CollisionProcessor()
rendering_processor = RenderingProcessor()
esper.add_processor(collision_processor, priority=2)
esper.add_processor(movement_processor, priority=3)
esper.add_processor(rendering_processor)
# or just add them in one line:
esper.add_processor(SomeProcessor())
通过单个调用 esper.process() 执行所有处理器。这将按照优先级顺序调用所有分配的处理器的方法。这通常在您的游戏每帧更新时(时钟的每个滴答)调用一次:
esper.process()
注意:你可以向esper.process()传递所需的所有参数(或关键字参数),但你也必须确保在处理器的process()方法中正确接收它们。例如,如果你将时间增量参数作为esper.process(dt)传递,你的处理器的process()方法应该都接收它如下:def process(self, dt): 这适用于像pyglet这样的库,它自动将时间增量值传递给计划中的函数。
通用用法
世界上下文
Esper支持多个“世界”上下文。导入时,一个“默认”世界是活动的。所有实体创建、处理器分配和其他操作都在活动世界的范围内进行。换句话说,世界上下文之间是完全隔离的。对于基本游戏和设计,你可能不需要担心这个功能。单个默认世界上下文通常就足够了。对于高级用例,例如当你的游戏中的不同场景有不同的实体和处理器需求时,这个功能非常有用。使用以下函数执行世界上下文操作:
- esper.list_worlds()
- esper.switch_world(name)
- esper.delete_world(name)
当切换世界时,请注意name
。如果一个世界不存在,当你第一次切换到它时,它将被创建。如果你不再需要它们,可以删除旧世界,但不能删除当前活动世界。
添加和删除处理器
你已经在前面的部分看到了添加处理器的示例。还有一个remove_processor函数可用
- esper.add_processor(processor_instance)
- esper.remove_processor(ProcessorClass)
根据你的游戏结构,你可能想在改变场景等情况下添加或删除某些处理器。
添加和删除组件
除了在创建实体时向其实体添加组件外,在处理器内添加或删除组件也是一种常见的模式。以下函数可用于此目的
- esper.add_component(entity_id, component_instance)
- esper.remove_component(entity_id, ComponentClass)
作为此示例,你可以有一个具有duration属性的“闪烁”组件。这可以用来让某些东西在一定时间内闪烁,然后消失。例如,下面的代码展示了在一个处理器中当实体受到伤害时添加此组件的简化情况。一个专门的BlinkProcessor处理效果,然后在持续时间过后删除组件:
class BlinkComponent:
def __init__(self, duration):
self.duration = duration
.....
class CollisionProcessor(esper.Processor):
def process(self, dt):
for ent, enemy in esper.get_component(Enemy):
...
is_damaged = self._some_method()
if is_damaged:
esper.add_component(ent, BlinkComponent(duration=1))
...
class BlinkProcessor(esper.Processor):
def process(self, dt):
for ent, (rend, blink) in esper.get_components(Renderable, BlinkComponent):
if blink.duration < 0:
# Times up. Remove the Component:
rend.sprite.visible = True
esper.remove_component(ent, BlinkComponent)
else:
blink.duration -= dt
# Toggle between visible and not visible each frame:
rend.sprite.visible = not rend.sprite.visible
查询特定组件
如果你有一个实体ID,并希望查询分配给它的一个特定组件或所有组件,以下函数可用
- esper.component_for_entity
- esper.components_for_entity
component_for_entity函数在有限的情况下很有用,当你知道一个特定的实体ID并希望为其获取一个特定的组件时。如果实体ID不存在组件,则会引发错误,所以它可能在与下一节中解释的has_component函数结合使用时更有用。例如:
if esper.has_component(ent, SFX):
sfx = esper.component_for_entity(ent, SFX)
sfx.play()
components_for_entity函数是一个特殊函数,它返回分配给特定实体的所有组件,作为一个元组。这是一个重型操作,你不会希望在每个帧或你的Processor.process
方法中执行它。但是,如果你想在两个不同的上下文之间(如切换场景或级别)转移特定实体的所有组件,它可能很有用。例如:
player_components = esper.components_for_entity(player_entity_id)
esper.switch_world('context_name')
player_entity_id = esper.create_entity(player_components)
布尔和条件检查
在某些情况下,你可能在执行某些操作之前希望检查实体是否具有特定的组件。以下函数可用于此任务
- esper.has_component(entity, ComponentType)
- esper.has_components(entity, ComponentTypeA, ComponentTypeB)
- esper.try_component(entity, ComponentType)
- esper.try_components(entity, ComponentTypeA, ComponentTypeB)
例如,您可能希望游戏中的弹体(仅弹体)在撞击墙壁时消失。我们可以通过检查实体是否有 Projectile
组件来实现这一点。我们不想对这个组件做任何操作,只是简单地检查它是否存在。考虑以下示例:
class CollisionProcessor(esper.Processor):
def process(self, dt):
for ent, body in esper.get_component(PhysicsBody):
...
colliding_with_wall = self._some_method(body):
if colliding_with_wall and esper.has_component(ent, Projectile):
esper.delete_entity(ent)
...
在另一种场景下,我们可能希望在实体组件上有操作时对实体组件执行一些操作。例如,一个跳过具有 Stun
组件的实体的运动处理器:
class MovementProcessor(esper.Processor):
def process(self, dt):
for ent, (body, vel) in esper.get_components(PhysicsBody, Velocity):
if esper.has_component(ent, Stun):
stun = esper.component_for_entity(ent, Stun)
stun.duration -= dt
if stun.duration <= 0:
esper.remove_component(ent, Stun)
continue # Continue to the next Entity
movement_code_here()
...
让我们看看代码的核心部分:
if esper.has_component(ent, Stun):
stun = esper.component_for_entity(ent, Stun)
stun.duration -= dt
这段代码可以正常工作,但 try_component 函数可以用一个更少的函数调用完成相同的事情。以下示例将获取存在的特定组件,如果不存在则返回 None:
stun = esper.try_component(ent, Stun)
if stun:
stun.duration -= dt
从 Python 3.8+ 开始,新的“walrus”操作符(:=
)也可以用于使 try_component 函数更加简洁:
if stun := esper.try_component(ent, Stun):
stun.duration -= dt
更多示例
查看 /examples 文件夹以了解游戏基本结构可能的样子。
事件分发
Esper 包括对事件分发和处理的基本支持。该功能由三个函数提供,用于设置(注册)、删除和分发事件。错误检查量很少,因此确保在分发和接收事件时使用正确的命名和参数数量由用户负责。
事件通过名称分发:
esper.dispatch_event('event_name', arg1, arg2)
为了接收上述事件,您必须注册处理程序。事件处理程序可以是函数或类方法。注册处理程序也是通过名称进行的:
esper.set_handler('event_name', my_func)
# or
esper.set_handler('event_name', self.my_method)
注意: 仅保留对注册处理程序的弱引用。如果处理程序被垃圾回收,它将自动通过内部回调取消注册。
处理程序也可以随时移除,如果您不再希望它们接收事件:
esper.remove_handler('event_name', my_func)
# or
esper.remove_handler('event_name', self.my_method)
已注册的事件和处理程序是当前 World
上下文的一部分。
贡献
对 Esper 的贡献总是受欢迎的,但有一些特定的项目目标需要记住
- 仅限纯 Python 代码:无二进制扩展、Cython 等。
- 尽量针对所有非 EOL Python 版本。如果存在充分理由,可以做出例外。
- 尽可能避免膨胀。如果新功能常用,则将考虑添加。一般来说,我们不希望添加其他模块或库可以更好地提供的服务功能。
- 性能优先于可读性。公共 API 应保持干净,但为了提供性能优势,丑陋的内部代码是可以接受的。每一圈都很重要!
在贡献之前有任何问题,请随时打开一个问题。
项目详情
下载文件
下载您平台的文件。如果您不确定选择哪个,请了解有关 安装软件包 的更多信息。
源代码分发
构建分发
esper-3.3.tar.gz 的哈希
算法 | 哈希摘要 | |
---|---|---|
SHA256 | f9b4aa08b65f2bbbf6f59db9df1fc4175b9c6a439ff9aba528f120506b666270 |
|
MD5 | 5467b0bb1e24c5f062b8660c85dac875 |
|
BLAKE2b-256 | 0892097b8777fd6596a6cefc4d2d0e14c2d0c905c2bf0da24b9814347a004820 |
esper-3.3-py3-none-any.whl 的哈希
算法 | 哈希摘要 | |
---|---|---|
SHA256 | 74e07f1339961329d35d8e794891a4f8f2a0a28e3deb8603574baaefefb8b90d |
|
MD5 | e4e9aaf81e922235980d3932d4f9f245 |
|
BLAKE2b-256 | 9523aa4a99dedd25a79b7cf78bebaf24e8ce4e1a8ff0ae2e361122f7f0d28cd6 |