# ECS Engine
This ECS Engine is a lightweight and dependancyless "Entity Component System" (ECS). ECSs are architectures that are most commonly used in video game because it allows you to manage a lot of concurrent states (components) and mutate these states efficently (systems).
## Getting Started
### Prerequisites
* Python 3.6 or newers
### Installation
To install this ECS Engine:
```
pip install ecs-engine==0.4.0
```
### Features
* **Entity, Components, System, and Admin**: Includes all of the features needed for a baseline ECS.
* **Entity Builder**: Is used to create builder objects to allow efficent object builder while making the process of creating entites more repeatable.
* **Singleton Component**: A singleton component to manage singular state that is used by 1 or more systems but not owned by any entities. Check out the [GDC talk by Overwatch's Tim Ford](https://www.youtube.com/watch?v=W3aieHjyNvw) for more info.
* **Component Pool**: An Object Pool for fast and efficient component creation. As well as using a "Sparse Set" data structure to improve entity caching and entity querying. [More on the Sparse Set here](https://stackoverflow.com/questions/23721645/designs-of-an-entity-component-system).
<sub>It is worth noting that the sparse set will increase memory overhead in exchange for performance.<sub>
* **EventBus**: An event bus to help provide system to system and admin to system communication.
### Quick start
Here is a quick example to get you started!
```
from ecs_engine import EcsAdmin, Entity, Component, SingletonComponent, System, subscribe_to_event
from dataclasses import dataclass
@dataclass
class PositionComponent(Component):
x: int
y: int
class UserComponent(Component):
...
@dataclass
class InputSingletonComponent(SingletonComponent):
input: KEY_PRESS
class UserMovementSystem(System):
required_components = [PositionComponent, UserComponent]
@subscribe_to_event('update_time_step')
def update(self, timestep: float):
entities = self.get_required_entities()
input_component = self.get_singleton_component(InputSingletonComponent)
for entity in entities:
if input_component.input == ['W']:
position_component = entity.get_component(PositionComponent)
position_component.y -= 1
class InputSystem(System):
@subscribe_to_event('keyboard_input')
def update(self, inputs):
input_singleton_component = self.get_singleton_component(InputSingletonComponent)
input_singleton_component.input = inputs
class CharacterBuilder(Builder):
def build_character(self, health, pos):
health_component = self.create_component(HealthComponent, health=health)
pos_component = self.create_component(PositionComponent, x=pos[0], y=pos[1])
components = [health_component, pos_component]
return self.build_entity(components)
class World(EcsAdmin):
systems = [UserMovementSystem]
events = ['update_time_step', 'keyboard_input']
singleton_components = [
InputSingletonComponent([])
]
builders = [CharacterBuilder]
def __init__(self):
super().__init__()
self.create_starting_entities()
def create_starting_entities(self):
self.get_builder(CharacterBuilder).build_character(health=100, pos=(0,0)
def timestep(self, time_step: float):
self.event_bus.publish('update_time_step', time_step)
def input(self, inputs: list[KeyboardEvent]):
self.event_bus.publish('keyboard_input', inputs)
# game loop
world = World()
world.add_singleton_component(InputComponent())
time_step = 1/TICK
while True:
keyboard_input = get_input()
world.input(keyboard_input)
world.timestep(time_step)
...
```
## Why use an ECS?
ECSs are considered highly performant and encourage decoupling.
There are many reasons that ECS are consider highly performant:
* Caching Efficiency
* Batch Processing
* Object Pooling
* Parallelism (GIL :sweat_smile:)
Decoupling is a natural and comfortable thing within an ECS, so much so that even if you saw performance loss compared to pure OOP architectures I'd still prefer ECS as it reducing bugs and spaghetti. Because you're force to make everything either a component (data structure) or a system (procedure) then they're all interchangeable. If I every need a new system that for combat I don't have to go and change any previous systems, I can just take the components and mutate them in an additional way.
## Definitions
### Components
Components are simply data structures that are used by the systems and entities to convey information. These components are dumb data structures the only methods they include are for serialization and deserialization.
Ex: You can have components to store infomration about position and health.
```
@dataclass
class PositionComponent(Component):
def init(self, x, y):
self.x = x
self.y = y
```
### Entities
An 'Entity' is an object that has and id and components attached to it. These components make up the features of the entity and define what it is. Without components an Entity is literally just an id with no attrbitutes at all.
Ex: You want to create a character. So you create an Entity and attach the following components: Position, Sprite, Health, Attack
```
character = Entity()
character.add_component(PositionComponent)
character.add_component(SpriteComponent)
character.add_component(HealthComponent)
character.add_component(AttackComponent)
```
### Systems
Systems process groups of entities but they don't process the entity obj but rather the components attached the the entity. A system will typically query for all of the entities that have components the system works with and then mutates the components inside.
Ex: You have an attacking system where you deal 10 damage and heal 5
```
class AttackSystem(System):
required_components = [AttackComponent]
@subscribe_to_event('update_time_step')
def update(self, timestep: float):
entities = self.get_required_entities()
for entity in entities:
attack_component = entity.get_component(AttackComponent)
entity_hit: Entity = self.check_collision_w_enemy(attack_component)
if entity_hit:
enemy_health_component = enemy_hit.get_component(HealthComponent)
entity_health_component = entity.get_component(HealthComponent)
enemy_health_component.health -= 10
entity_health_component.health += 5
self.event_bus.publish('check_for_death', opponent_hit)
```
### Events
While not always used in ECS systems; this engine here does use events and listeners to communicate between the Admin and System to System. These events could be as common as timesteps of timedeltas for new frames to events triggered by systems like setting off a tnt in the game world.
Ex: In the previous system example you get two exapmles of event system. The system is updated by the time_step that happens every frame in the game. At the end it publishes and event to alert any subscribers that this character may die.
##
### Documentation
[Documentation can be found here](https://jsimerly.github.io/ecs_engine/). It's built using sphinx and everything in the documentation can be found in the comments of the source code.
### License
ECS Engine is licensed under the MIT License
Raw data
{
"_id": null,
"home_page": "https://github.com/jsimerly/ecs_engine",
"name": "ecs-engine",
"maintainer": "",
"docs_url": null,
"requires_python": "",
"maintainer_email": "",
"keywords": "ECS Entity Component System pygame",
"author": "Jacob Simerly",
"author_email": "simerly81@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/71/2a/641556d3c5a6d4f3749625f8326a83a6cd277ec186b13ecf392f941f12ca/ecs_engine-0.5.4.tar.gz",
"platform": null,
"description": "# ECS Engine\r\nThis ECS Engine is a lightweight and dependancyless \"Entity Component System\" (ECS). ECSs are architectures that are most commonly used in video game because it allows you to manage a lot of concurrent states (components) and mutate these states efficently (systems). \r\n\r\n## Getting Started\r\n### Prerequisites\r\n* Python 3.6 or newers\r\n \r\n### Installation\r\nTo install this ECS Engine: \r\n```\r\npip install ecs-engine==0.4.0\r\n```\r\n\r\n### Features\r\n* **Entity, Components, System, and Admin**: Includes all of the features needed for a baseline ECS.\r\n* **Entity Builder**: Is used to create builder objects to allow efficent object builder while making the process of creating entites more repeatable.\r\n* **Singleton Component**: A singleton component to manage singular state that is used by 1 or more systems but not owned by any entities. Check out the [GDC talk by Overwatch's Tim Ford](https://www.youtube.com/watch?v=W3aieHjyNvw) for more info.\r\n* **Component Pool**: An Object Pool for fast and efficient component creation. As well as using a \"Sparse Set\" data structure to improve entity caching and entity querying. [More on the Sparse Set here](https://stackoverflow.com/questions/23721645/designs-of-an-entity-component-system).\r\n\r\n <sub>It is worth noting that the sparse set will increase memory overhead in exchange for performance.<sub>\r\n* **EventBus**: An event bus to help provide system to system and admin to system communication.\r\n\r\n\r\n### Quick start\r\nHere is a quick example to get you started!\r\n```\r\nfrom ecs_engine import EcsAdmin, Entity, Component, SingletonComponent, System, subscribe_to_event\r\nfrom dataclasses import dataclass\r\n\r\n@dataclass\r\nclass PositionComponent(Component):\r\n x: int\r\n y: int\r\n\r\nclass UserComponent(Component):\r\n ...\r\n\r\n@dataclass\r\nclass InputSingletonComponent(SingletonComponent):\r\n input: KEY_PRESS\r\n\r\nclass UserMovementSystem(System):\r\n required_components = [PositionComponent, UserComponent]\r\n\r\n @subscribe_to_event('update_time_step')\r\n def update(self, timestep: float):\r\n entities = self.get_required_entities()\r\n input_component = self.get_singleton_component(InputSingletonComponent)\r\n\r\n for entity in entities:\r\n if input_component.input == ['W']:\r\n position_component = entity.get_component(PositionComponent)\r\n position_component.y -= 1\r\n\r\nclass InputSystem(System):\r\n @subscribe_to_event('keyboard_input')\r\n def update(self, inputs):\r\n input_singleton_component = self.get_singleton_component(InputSingletonComponent)\r\n input_singleton_component.input = inputs\r\n\r\nclass CharacterBuilder(Builder):\r\n def build_character(self, health, pos):\r\n health_component = self.create_component(HealthComponent, health=health)\r\n pos_component = self.create_component(PositionComponent, x=pos[0], y=pos[1])\r\n\r\n components = [health_component, pos_component]\r\n return self.build_entity(components)\r\n\r\nclass World(EcsAdmin):\r\n systems = [UserMovementSystem]\r\n events = ['update_time_step', 'keyboard_input']\r\n singleton_components = [\r\n InputSingletonComponent([])\r\n ]\r\n builders = [CharacterBuilder]\r\n\r\n def __init__(self):\r\n super().__init__()\r\n self.create_starting_entities()\r\n\r\n def create_starting_entities(self):\r\n self.get_builder(CharacterBuilder).build_character(health=100, pos=(0,0)\r\n\r\n def timestep(self, time_step: float):\r\n self.event_bus.publish('update_time_step', time_step)\r\n\r\n def input(self, inputs: list[KeyboardEvent]):\r\n self.event_bus.publish('keyboard_input', inputs)\r\n\r\n# game loop\r\nworld = World()\r\nworld.add_singleton_component(InputComponent())\r\ntime_step = 1/TICK\r\n\r\nwhile True:\r\n keyboard_input = get_input()\r\n world.input(keyboard_input)\r\n world.timestep(time_step)\r\n ...\r\n \r\n``` \r\n \r\n## Why use an ECS?\r\nECSs are considered highly performant and encourage decoupling. \r\n\r\nThere are many reasons that ECS are consider highly performant:\r\n* Caching Efficiency\r\n* Batch Processing\r\n* Object Pooling\r\n* Parallelism (GIL :sweat_smile:)\r\n\r\nDecoupling is a natural and comfortable thing within an ECS, so much so that even if you saw performance loss compared to pure OOP architectures I'd still prefer ECS as it reducing bugs and spaghetti. Because you're force to make everything either a component (data structure) or a system (procedure) then they're all interchangeable. If I every need a new system that for combat I don't have to go and change any previous systems, I can just take the components and mutate them in an additional way.\r\n\r\n## Definitions\r\n### Components\r\nComponents are simply data structures that are used by the systems and entities to convey information. These components are dumb data structures the only methods they include are for serialization and deserialization. \r\n\r\nEx: You can have components to store infomration about position and health.\r\n```\r\n@dataclass\r\nclass PositionComponent(Component):\r\n def init(self, x, y):\r\n self.x = x\r\n self.y = y\r\n```\r\n\r\n### Entities\r\nAn 'Entity' is an object that has and id and components attached to it. These components make up the features of the entity and define what it is. Without components an Entity is literally just an id with no attrbitutes at all.\r\n\r\nEx: You want to create a character. So you create an Entity and attach the following components: Position, Sprite, Health, Attack\r\n```\r\ncharacter = Entity()\r\ncharacter.add_component(PositionComponent)\r\ncharacter.add_component(SpriteComponent)\r\ncharacter.add_component(HealthComponent)\r\ncharacter.add_component(AttackComponent)\r\n```\r\n\r\n### Systems\r\nSystems process groups of entities but they don't process the entity obj but rather the components attached the the entity. A system will typically query for all of the entities that have components the system works with and then mutates the components inside.\r\n\r\nEx: You have an attacking system where you deal 10 damage and heal 5\r\n```\r\nclass AttackSystem(System):\r\n required_components = [AttackComponent]\r\n \r\n @subscribe_to_event('update_time_step')\r\n def update(self, timestep: float):\r\n entities = self.get_required_entities()\r\n for entity in entities:\r\n attack_component = entity.get_component(AttackComponent)\r\n entity_hit: Entity = self.check_collision_w_enemy(attack_component)\r\n \r\n if entity_hit:\r\n enemy_health_component = enemy_hit.get_component(HealthComponent)\r\n entity_health_component = entity.get_component(HealthComponent)\r\n enemy_health_component.health -= 10\r\n entity_health_component.health += 5\r\n \r\n self.event_bus.publish('check_for_death', opponent_hit)\r\n```\r\n\r\n### Events\r\nWhile not always used in ECS systems; this engine here does use events and listeners to communicate between the Admin and System to System. These events could be as common as timesteps of timedeltas for new frames to events triggered by systems like setting off a tnt in the game world.\r\n\r\nEx: In the previous system example you get two exapmles of event system. The system is updated by the time_step that happens every frame in the game. At the end it publishes and event to alert any subscribers that this character may die.\r\n\r\n## \r\n### Documentation\r\n[Documentation can be found here](https://jsimerly.github.io/ecs_engine/). It's built using sphinx and everything in the documentation can be found in the comments of the source code.\r\n\r\n### License \r\nECS Engine is licensed under the MIT License\r\n\r\n\r\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "This is an ECS Engine for building game in python.",
"version": "0.5.4",
"project_urls": {
"Homepage": "https://github.com/jsimerly/ecs_engine"
},
"split_keywords": [
"ecs",
"entity",
"component",
"system",
"pygame"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "e28e78e5cd92db2c482356aabb55c4fa9120417403fa81e15ab7a7978f8c2a9c",
"md5": "c52682e719a947e6cae4bcf50e83ca2c",
"sha256": "f0f3e260929906fcd81832ddcc10c17e91842d2e17f2c6d51c8d304eb7ac5bd3"
},
"downloads": -1,
"filename": "ecs_engine-0.5.4-py3-none-any.whl",
"has_sig": false,
"md5_digest": "c52682e719a947e6cae4bcf50e83ca2c",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": null,
"size": 18771,
"upload_time": "2024-03-12T01:44:35",
"upload_time_iso_8601": "2024-03-12T01:44:35.827524Z",
"url": "https://files.pythonhosted.org/packages/e2/8e/78e5cd92db2c482356aabb55c4fa9120417403fa81e15ab7a7978f8c2a9c/ecs_engine-0.5.4-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "712a641556d3c5a6d4f3749625f8326a83a6cd277ec186b13ecf392f941f12ca",
"md5": "e55044f348098c6339cf646091fb8631",
"sha256": "9a7243529ec94ec96d5e1de9691b1727a16abaef293e957c492730a933bdfd7d"
},
"downloads": -1,
"filename": "ecs_engine-0.5.4.tar.gz",
"has_sig": false,
"md5_digest": "e55044f348098c6339cf646091fb8631",
"packagetype": "sdist",
"python_version": "source",
"requires_python": null,
"size": 16923,
"upload_time": "2024-03-12T01:44:37",
"upload_time_iso_8601": "2024-03-12T01:44:37.418840Z",
"url": "https://files.pythonhosted.org/packages/71/2a/641556d3c5a6d4f3749625f8326a83a6cd277ec186b13ecf392f941f12ca/ecs_engine-0.5.4.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2024-03-12 01:44:37",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "jsimerly",
"github_project": "ecs_engine",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "ecs-engine"
}