├── tests ├── __init__.py ├── test_components.py ├── test_group.py ├── test_context.py ├── test_matcher.py ├── test_entity.py └── test_entity_index.py ├── entitas ├── __version__.py ├── __init__.py ├── utils.py ├── exceptions.py ├── matcher.py ├── collector.py ├── entity_index.py ├── context.py ├── group.py ├── processors.py └── entity.py ├── Pipfile ├── tox.ini ├── .gitignore ├── .travis.yml ├── setup.py ├── LICENSE ├── Pipfile.lock └── README.rst /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /entitas/__version__.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.0.1' 2 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [dev-packages] 2 | pytest = "*" 3 | tox = "*" 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | 3 | [testenv] 4 | deps = 5 | pytest 6 | commands = 7 | pytest -s -v 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python 2 | __pycache__/ 3 | build/ 4 | dist/ 5 | *.pyc 6 | *.egg-info/ 7 | 8 | # Py.test 9 | .cache/ 10 | 11 | # tox 12 | .tox/ 13 | 14 | # OS 15 | Thumbs.db 16 | .DS_Store 17 | -------------------------------------------------------------------------------- /tests/test_components.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | Movable = namedtuple('Movable', '') 4 | Position = namedtuple('Position', 'x y') 5 | Person = namedtuple('Person', 'name age') 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.5" 4 | - "3.6" 5 | 6 | # command to install dependencies 7 | install: pip install pipenv; pipenv lock; pipenv install --dev 8 | # command to run tests 9 | script: pipenv run pytest tests/ 10 | -------------------------------------------------------------------------------- /entitas/__init__.py: -------------------------------------------------------------------------------- 1 | from .entity import Entity 2 | from .entity_index import PrimaryEntityIndex, EntityIndex 3 | from .context import Context 4 | from .matcher import Matcher 5 | from .group import Group, GroupEvent 6 | from .collector import Collector 7 | from .processors import ( 8 | Processors, InitializeProcessor, ExecuteProcessor, CleanupProcessor, 9 | TearDownProcessor, ReactiveProcessor 10 | ) 11 | from .utils import Event 12 | from .exceptions import ( 13 | AlreadyAddedComponent, MissingComponent, MissingEntity, GroupSingleEntity, 14 | EntitasException 15 | ) 16 | -------------------------------------------------------------------------------- /entitas/utils.py: -------------------------------------------------------------------------------- 1 | class Event(object): 2 | """C# events in Python.""" 3 | 4 | def __init__(self): 5 | self._listeners = [] 6 | 7 | def __call__(self, *args, **kwargs): 8 | for listener in self._listeners: 9 | listener(*args, **kwargs) 10 | 11 | def __add__(self, listener): 12 | if listener not in self._listeners: 13 | self._listeners.append(listener) 14 | return self 15 | 16 | def __sub__(self, listener): 17 | if listener in self._listeners: 18 | self._listeners.remove(listener) 19 | return self 20 | -------------------------------------------------------------------------------- /entitas/exceptions.py: -------------------------------------------------------------------------------- 1 | class EntityNotEnabled(Exception): 2 | """The entity is not enabled.""" 3 | 4 | 5 | class AlreadyAddedComponent(Exception): 6 | """The entity already contains this type of component.""" 7 | 8 | 9 | class MissingComponent(Exception): 10 | """The entity does not contain this type of component.""" 11 | 12 | 13 | class MissingEntity(Exception): 14 | """The context does not contain this entity.""" 15 | 16 | 17 | class GroupSingleEntity(Exception): 18 | """The group contains more than one entity.""" 19 | 20 | 21 | class EntitasException(Exception): 22 | def __init__(self, message, hint): 23 | super().__init__(message + '\n' + hint if hint else message) 24 | -------------------------------------------------------------------------------- /tests/test_group.py: -------------------------------------------------------------------------------- 1 | from entitas import Context, Matcher 2 | from .test_components import Movable 3 | 4 | _context = Context() 5 | _entity = _context.create_entity() 6 | _entity.add(Movable) 7 | _group = _context.get_group(Matcher(Movable)) 8 | 9 | 10 | class TestGroup(object): 11 | 12 | def test_entities(self): 13 | assert len(_group.entities) == 1 14 | 15 | def test_single_entity(self): 16 | assert _group.single_entity.has(Movable) 17 | 18 | def test_events(self): 19 | assert _group.single_entity == _entity 20 | _entity.replace(Movable) 21 | assert _group.single_entity == _entity 22 | _entity.remove(Movable) 23 | assert not _group.single_entity 24 | -------------------------------------------------------------------------------- /entitas/matcher.py: -------------------------------------------------------------------------------- 1 | def get_expr_repr(expr): 2 | return '' if expr is None else ','.join([x.__name__ for x in expr]) 3 | 4 | 5 | class Matcher(object): 6 | 7 | def __init__(self, *args, **kwargs): 8 | self._all = args if args else kwargs.get('all_of', None) 9 | self._any = kwargs.get('any_of', None) 10 | self._none = kwargs.get('none_of', None) 11 | 12 | def matches(self, entity): 13 | all_cond = self._all is None or entity.has(*self._all) 14 | any_cond = self._any is None or entity.has_any(*self._any) 15 | none_cond = self._none is None or not entity.has_any(*self._none) 16 | 17 | return all_cond and any_cond and none_cond 18 | 19 | def __repr__(self): 20 | return ''.format( 21 | get_expr_repr(self._all), 22 | get_expr_repr(self._any), 23 | get_expr_repr(self._none)) 24 | -------------------------------------------------------------------------------- /tests/test_context.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from entitas import Context, Entity, MissingEntity 3 | 4 | _context = Context() 5 | _entity = _context.create_entity() 6 | 7 | 8 | class TestContext(object): 9 | 10 | def test_entry_points(self): 11 | Context.create_entity 12 | Context.has_entity 13 | Context.destroy_entity 14 | Context.entities 15 | Context.get_group 16 | Context.set_unique_component 17 | Context.get_unique_component 18 | 19 | def test_has_entity(self): 20 | assert _context.has_entity(_entity) 21 | assert isinstance(_entity, Entity) 22 | 23 | def test_entities(self): 24 | assert len(_context.entities) == 1 25 | 26 | def test_destroy_entity(self): 27 | _context.destroy_entity(_entity) 28 | assert not _context.has_entity(_entity) 29 | 30 | with pytest.raises(MissingEntity): 31 | _context.destroy_entity(_entity) 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | 5 | from setuptools import setup 6 | 7 | 8 | here = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | with open(os.path.join(here, 'README.rst'), encoding='utf-8') as f: 11 | long_description = '\n' + f.read() 12 | 13 | required = [] 14 | packages = ['entitas'] 15 | 16 | # About dict to store version and package info 17 | about = dict() 18 | version_path = os.path.join(here, 'entitas', '__version__.py') 19 | with open(version_path, 'r', encoding='utf-8') as f: 20 | exec(f.read(), about) 21 | 22 | setup( 23 | name='Entitas', 24 | version=about['__version__'], 25 | description='Entitas ECS implementation in Python.', 26 | long_description=long_description, 27 | author='Fabien Nouaillat', 28 | author_email='aenyhm@gmail.com', 29 | url='https://github.com/aenyhm/entitas-python', 30 | packages=packages, 31 | install_requires=required, 32 | license='MIT', 33 | ) 34 | -------------------------------------------------------------------------------- /tests/test_matcher.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | from entitas import Entity, Matcher 3 | 4 | CompA = namedtuple('CompA', '') 5 | CompB = namedtuple('CompB', '') 6 | CompC = namedtuple('CompC', '') 7 | CompD = namedtuple('CompD', '') 8 | CompE = namedtuple('CompE', '') 9 | CompF = namedtuple('CompF', '') 10 | 11 | 12 | def test_matches(): 13 | ea = Entity() 14 | eb = Entity() 15 | ec = Entity() 16 | ea.activate(0) 17 | eb.activate(1) 18 | ec.activate(2) 19 | ea.add(CompA) 20 | ea.add(CompB) 21 | ea.add(CompC) 22 | ea.add(CompE) 23 | eb.add(CompA) 24 | eb.add(CompB) 25 | eb.add(CompC) 26 | eb.add(CompE) 27 | eb.add(CompF) 28 | ec.add(CompB) 29 | ec.add(CompC) 30 | ec.add(CompD) 31 | 32 | matcher = Matcher(all_of=[CompA, CompB, CompC], 33 | any_of=[CompD, CompE], 34 | none_of=[CompF]) 35 | 36 | assert matcher.matches(ea) 37 | assert not matcher.matches(eb) 38 | assert not matcher.matches(ec) 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fabien Nouaillat 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /entitas/collector.py: -------------------------------------------------------------------------------- 1 | from .group import GroupEvent 2 | 3 | 4 | class Collector(object): 5 | 6 | def __init__(self): 7 | self._collected_entities = set() 8 | self._groups = {} 9 | 10 | @property 11 | def collected_entities(self): 12 | return self._collected_entities 13 | 14 | def add(self, group, group_event): 15 | self._groups[group] = group_event 16 | 17 | def activate(self): 18 | for group in self._groups: 19 | group_event = self._groups[group] 20 | 21 | added_event = group_event == GroupEvent.added 22 | removed_event = group_event == GroupEvent.removed 23 | added_or_removed_event = group_event == GroupEvent.added_or_removed 24 | 25 | if added_event or added_or_removed_event: 26 | group.on_entity_added -= self._add_entity 27 | group.on_entity_added += self._add_entity 28 | 29 | if removed_event or added_or_removed_event: 30 | group.on_entity_removed -= self._add_entity 31 | group.on_entity_removed += self._add_entity 32 | 33 | def deactivate(self): 34 | for group in self._groups: 35 | group.on_entity_added -= self._add_entity 36 | group.on_entity_removed -= self._add_entity 37 | 38 | self.clear_collected_entities() 39 | 40 | def clear_collected_entities(self): 41 | self._collected_entities.clear() 42 | 43 | def _add_entity(self, entity): # , component 44 | self._collected_entities.add(entity) 45 | 46 | def __repr__(self): 47 | return ''.format( 114 | len(self._entities), len(self._reusable_entities)) 115 | -------------------------------------------------------------------------------- /entitas/group.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from .utils import Event 4 | from .exceptions import GroupSingleEntity 5 | 6 | 7 | class GroupEvent(Enum): 8 | ADDED = 1 9 | REMOVED = 2 10 | ADDED_OR_REMOVED = 3 11 | 12 | 13 | class Group(object): 14 | """Use context.get_group(matcher) to get a group of entities which 15 | match the specified matcher. Calling context.get_group(matcher) with 16 | the same matcher will always return the same instance of the group. 17 | 18 | The created group is managed by the context and will always be up to 19 | date. It will automatically add entities that match the matcher or 20 | remove entities as soon as they don't match the matcher anymore. 21 | """ 22 | 23 | def __init__(self, matcher): 24 | 25 | #: Occurs when an entity gets added. 26 | self.on_entity_added = Event() 27 | 28 | #: Occurs when an entity gets removed. 29 | self.on_entity_removed = Event() 30 | 31 | #: Occurs when a component of an entity in the group gets 32 | # replaced. 33 | self.on_entity_updated = Event() 34 | 35 | self._matcher = matcher 36 | self._entities = set() 37 | 38 | @property 39 | def entities(self): 40 | return self._entities 41 | 42 | @property 43 | def single_entity(self): 44 | """Returns the only entity in this group. 45 | It will return None if the group is empty. 46 | It will throw a :class:`MissingComponent` if the group has more 47 | than one entity. 48 | """ 49 | count = len(self._entities) 50 | 51 | if count == 1: 52 | return min(self._entities) 53 | if count == 0: 54 | return None 55 | 56 | raise GroupSingleEntity( 57 | 'Cannot get a single entity from a group containing {} entities.', 58 | len(self._entities)) 59 | 60 | def handle_entity_silently(self, entity): 61 | """This is used by the context to manage the group. 62 | :param matcher: Entity 63 | """ 64 | if self._matcher.matches(entity): 65 | self._add_entity_silently(entity) 66 | else: 67 | self._remove_entity_silently(entity) 68 | 69 | def handle_entity(self, entity, component): 70 | """This is used by the context to manage the group. 71 | :param matcher: Entity 72 | """ 73 | if self._matcher.matches(entity): 74 | self._add_entity(entity, component) 75 | else: 76 | self._remove_entity(entity, component) 77 | 78 | def update_entity(self, entity, previous_comp, new_comp): 79 | """This is used by the context to manage the group. 80 | :param matcher: Entity 81 | """ 82 | if entity in self._entities: 83 | self.on_entity_removed(entity, previous_comp) 84 | self.on_entity_added(entity, new_comp) 85 | self.on_entity_updated(entity, previous_comp, new_comp) 86 | 87 | def _add_entity_silently(self, entity): 88 | if entity not in self._entities: 89 | self._entities.add(entity) 90 | return True 91 | return False 92 | 93 | def _add_entity(self, entity, component): 94 | entity_added = self._add_entity_silently(entity) 95 | if entity_added: 96 | self.on_entity_added(entity, component) 97 | 98 | def _remove_entity_silently(self, entity): 99 | if entity in self._entities: 100 | self._entities.remove(entity) 101 | return True 102 | return False 103 | 104 | def _remove_entity(self, entity, component): 105 | entity_removed = self._remove_entity_silently(entity) 106 | if entity_removed: 107 | self.on_entity_removed(entity, component) 108 | 109 | def __repr__(self): 110 | return ''.format(self._matcher) 111 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Entitas for Python 2 | ================== 3 | 4 | .. image:: https://travis-ci.org/Aenyhm/entitas-python.svg?branch=master 5 | :target: https://travis-ci.org/Aenyhm/entitas-python 6 | 7 | entitas-python is a port of `Entitas ECS for C# and Unity`_. 8 | 9 | 10 | Overview 11 | -------- 12 | 13 | Components 14 | ~~~~~~~~~~ 15 | 16 | .. code-block:: python 17 | 18 | Position = namedtuple('Position', 'x y') 19 | Health = namedtuple('Health', 'value') 20 | Movable = namedtuple('Movable', '') 21 | 22 | Entity 23 | ~~~~~~ 24 | 25 | .. code-block:: python 26 | 27 | entity.add(Position, 3, 7) 28 | entity.add(Health, 100) 29 | entity.add(Movable) 30 | 31 | entity.replace(Position, 10, 100) 32 | entity.replace(Health, entity.get(Health).value - 1) 33 | 34 | entity.remove(Position) 35 | 36 | has_pos = entity.has(Position) 37 | movable = entity.has(Movable) 38 | 39 | Context 40 | ~~~~~~~ 41 | 42 | .. code-block:: python 43 | 44 | context = Context() 45 | entity = context.create_entity() 46 | entity.add(Movable) 47 | 48 | entities = context.entities 49 | for e in entities: 50 | # do something 51 | 52 | Group 53 | ~~~~~ 54 | 55 | .. code-block:: python 56 | 57 | context.get_group(Matcher(Position)).entities 58 | 59 | def move(entity, new_comp): 60 | # do something 61 | 62 | context.get_group(Matcher(Position)).on_entity_added += move 63 | 64 | Entity Collector 65 | ~~~~~~~~~~~~~~~~ 66 | 67 | .. code-block:: python 68 | 69 | group = context.get_group(Matcher(Position)) 70 | collector = Collector() 71 | collector.add(group, GroupEvent.added) 72 | 73 | # later 74 | 75 | for e in collector.collected_entities: 76 | # do something with all the entities 77 | # that have been collected to this point of time 78 | 79 | collector.clear_collected_entities() 80 | 81 | # stop observing 82 | collector.deactivate() 83 | 84 | Entity Index 85 | ~~~~~~~~~~~~ 86 | 87 | .. code-block:: python 88 | 89 | Person = namedtuple('Person', 'name age') 90 | group = context.get_group(Matcher(Person)) 91 | 92 | # get a set of 42-year-old Person 93 | index = EntityIndex(Person, group, 'age') 94 | context.add_entity_index(index) 95 | entities = context.get_entity_index(Person).get_entities(42) 96 | 97 | # get the Person named "John" 98 | primary_index = PrimaryEntityIndex(Person, group, 'name') 99 | context.add_entity_index(primary_index) 100 | entity = context.get_entity_index(Person).get_entity('John') 101 | 102 | Processors 103 | ~~~~~~~~~~ 104 | 105 | .. code-block:: python 106 | 107 | class RenderDisplay(ExecuteProcessor): 108 | 109 | def execute(self): 110 | pygame.display.update() 111 | 112 | 113 | # Initialize, Cleanup and TearDown are also available. 114 | 115 | 116 | class Move(ReactiveProcessor): 117 | 118 | def __init__(self, context): 119 | super().__init__(context) 120 | self._context = context 121 | 122 | def get_trigger(self): 123 | return {Matcher(Position): GroupEvent.ADDED} 124 | 125 | def filter(self, entity): 126 | return entity.has(Position, Movable) 127 | 128 | def react(self, entities): 129 | for entity in entities: 130 | # use entity.get(Position).x & entity.get(Position).y 131 | 132 | Setup example 133 | ~~~~~~~~~~~~~ 134 | 135 | .. code-block:: python 136 | 137 | context = Context() 138 | 139 | processors = Processors() 140 | processors.add(StartGame(context)) 141 | processors.add(InputProcessors(context)) 142 | processors.add(RenderDisplay()) 143 | processors.add(DestroyEntity(context)) 144 | 145 | processors.initialize() 146 | processors.activate_reactive_processors() 147 | 148 | # main loop 149 | running = True 150 | while running: 151 | processors.execute() 152 | processors.cleanup() 153 | 154 | if EmitInput.quit: 155 | break 156 | 157 | processors.clear_reactive_processors() 158 | processors.tear_down() 159 | 160 | quit() 161 | 162 | 163 | .. _Entitas ECS for C# and Unity : https://github.com/sschmid/Entitas-CSharp 164 | -------------------------------------------------------------------------------- /entitas/processors.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | from .collector import Collector 4 | 5 | 6 | class InitializeProcessor(metaclass=ABCMeta): 7 | @abstractmethod 8 | def initialize(self): 9 | pass 10 | 11 | 12 | class ExecuteProcessor(metaclass=ABCMeta): 13 | @abstractmethod 14 | def execute(self): 15 | pass 16 | 17 | 18 | class CleanupProcessor(metaclass=ABCMeta): 19 | @abstractmethod 20 | def cleanup(self): 21 | pass 22 | 23 | 24 | class TearDownProcessor(metaclass=ABCMeta): 25 | @abstractmethod 26 | def tear_down(self): 27 | pass 28 | 29 | 30 | class ReactiveProcessor(ExecuteProcessor): 31 | 32 | def __init__(self, context): 33 | self._collector = self._get_collector(context) 34 | self._buffer = [] 35 | 36 | @abstractmethod 37 | def get_trigger(self): 38 | pass 39 | 40 | @abstractmethod 41 | def filter(self, entity): 42 | pass 43 | 44 | @abstractmethod 45 | def react(self, entities): 46 | pass 47 | 48 | def activate(self): 49 | self._collector.activate() 50 | 51 | def deactivate(self): 52 | self._collector.deactivate() 53 | 54 | def clear(self): 55 | self._collector.clear_collected_entities() 56 | 57 | def execute(self): 58 | if self._collector.collected_entities: 59 | for entity in self._collector.collected_entities: 60 | if self.filter(entity): 61 | self._buffer.append(entity) 62 | 63 | self._collector.clear_collected_entities() 64 | 65 | if self._buffer: 66 | self.react(self._buffer) 67 | self._buffer.clear() 68 | 69 | def _get_collector(self, context): 70 | trigger = self.get_trigger() 71 | collector = Collector() 72 | 73 | for matcher in trigger: 74 | group_event = trigger[matcher] 75 | group = context.get_group(matcher) 76 | collector.add(group, group_event) 77 | 78 | return collector 79 | 80 | 81 | class Processors(InitializeProcessor, ExecuteProcessor, 82 | CleanupProcessor, TearDownProcessor): 83 | 84 | def __init__(self): 85 | self._initialize_processors = [] 86 | self._execute_processors = [] 87 | self._cleanup_processors = [] 88 | self._tear_down_processors = [] 89 | 90 | def add(self, processor): 91 | if isinstance(processor, InitializeProcessor): 92 | self._initialize_processors.append(processor) 93 | 94 | if isinstance(processor, ExecuteProcessor): 95 | self._execute_processors.append(processor) 96 | 97 | if isinstance(processor, CleanupProcessor): 98 | self._cleanup_processors.append(processor) 99 | 100 | if isinstance(processor, TearDownProcessor): 101 | self._tear_down_processors.append(processor) 102 | 103 | def initialize(self): 104 | for processor in self._initialize_processors: 105 | processor.initialize() 106 | 107 | def execute(self): 108 | for processor in self._execute_processors: 109 | processor.execute() 110 | 111 | def cleanup(self): 112 | for processor in self._cleanup_processors: 113 | processor.cleanup() 114 | 115 | def tear_down(self): 116 | for processor in self._tear_down_processors: 117 | processor.tear_down() 118 | 119 | def activate_reactive_processors(self): 120 | for processor in self._execute_processors: 121 | if isinstance(processor, ReactiveProcessor): 122 | processor.activate() 123 | 124 | if isinstance(processor, Processors): 125 | processor.activate_reactive_processors() 126 | 127 | def deactivate_reactive_processors(self): 128 | for processor in self._execute_processors: 129 | if issubclass(processor, ReactiveProcessor): 130 | processor.deactivate() 131 | 132 | if isinstance(processor, Processors): 133 | processor.deactivate_reactive_processors() 134 | 135 | def clear_reactive_processors(self): 136 | for processor in self._execute_processors: 137 | if issubclass(processor, ReactiveProcessor): 138 | processor.clear() 139 | 140 | if isinstance(processor, Processors): 141 | processor.clear_reactive_processors() 142 | -------------------------------------------------------------------------------- /entitas/entity.py: -------------------------------------------------------------------------------- 1 | """ 2 | entitas.entity 3 | ~~~~~~~~~~~~~~ 4 | An entity is a container holding data to represent certain 5 | objects in your application. You can add, replace or remove data 6 | from entities. 7 | 8 | Those containers are called 'components'. They are represented by 9 | namedtuples for readability. 10 | """ 11 | 12 | from .utils import Event 13 | from .exceptions import ( 14 | EntityNotEnabled, AlreadyAddedComponent, MissingComponent) 15 | 16 | 17 | class Entity(object): 18 | """Use context.create_entity() to create a new entity and 19 | context.destroy_entity() to destroy it. 20 | You can add, replace and remove components to an entity. 21 | """ 22 | 23 | def __init__(self): 24 | 25 | #: Occurs when a component gets added. 26 | self.on_component_added = Event() 27 | 28 | #: Occurs when a component gets removed. 29 | self.on_component_removed = Event() 30 | 31 | #: Occurs when a component gets replaced. 32 | self.on_component_replaced = Event() 33 | 34 | #: Dictionary mapping component type and component instance. 35 | self._components = {} 36 | 37 | #: Each entity has its own unique creationIndex which will be 38 | #: set by the context when you create the entity. 39 | self._creation_index = 0 40 | 41 | #: The context manages the state of an entity. 42 | #: Active entities are enabled, destroyed entities are not. 43 | self._is_enabled = False 44 | 45 | def activate(self, creation_index): 46 | self._creation_index = creation_index 47 | self._is_enabled = True 48 | 49 | def add(self, comp_type, *args): 50 | """Adds a component. 51 | :param comp_type: namedtuple type 52 | :param *args: (optional) data values 53 | """ 54 | if not self._is_enabled: 55 | raise EntityNotEnabled( 56 | 'Cannot add component {!r}: {} is not enabled.' 57 | .format(comp_type.__name__, self)) 58 | 59 | if self.has(comp_type): 60 | raise AlreadyAddedComponent( 61 | 'Cannot add another component {!r} to {}.' 62 | .format(comp_type.__name__, self)) 63 | 64 | new_comp = comp_type._make(args) 65 | self._components[comp_type] = new_comp 66 | self.on_component_added(self, new_comp) 67 | 68 | def remove(self, comp_type): 69 | """Removes a component. 70 | :param comp_type: namedtuple type 71 | """ 72 | if not self._is_enabled: 73 | raise EntityNotEnabled( 74 | 'Cannot remove component {!r}: {} is not enabled.' 75 | .format(comp_type.__name__, self)) 76 | 77 | if not self.has(comp_type): 78 | raise MissingComponent( 79 | 'Cannot remove unexisting component {!r} from {}.' 80 | .format(comp_type.__name__, self)) 81 | 82 | self._replace(comp_type, None) 83 | 84 | def replace(self, comp_type, *args): 85 | """Replaces an existing component or adds it if it doesn't exist 86 | yet. 87 | :param comp_type: namedtuple type 88 | :param *args: (optional) data values 89 | """ 90 | if not self._is_enabled: 91 | raise EntityNotEnabled( 92 | 'Cannot replace component {!r}: {} is not enabled.' 93 | .format(comp_type.__name__, self)) 94 | 95 | if self.has(comp_type): 96 | self._replace(comp_type, args) 97 | else: 98 | self.add(comp_type, *args) 99 | 100 | def _replace(self, comp_type, args): 101 | previous_comp = self._components[comp_type] 102 | if args is None: 103 | del self._components[comp_type] 104 | self.on_component_removed(self, previous_comp) 105 | else: 106 | new_comp = comp_type._make(args) 107 | self._components[comp_type] = new_comp 108 | self.on_component_replaced(self, previous_comp, new_comp) 109 | 110 | def get(self, comp_type): 111 | """Retrieves a component by its type. 112 | :param comp_type: namedtuple type 113 | :rtype: namedtuple 114 | """ 115 | if not self.has(comp_type): 116 | raise MissingComponent( 117 | 'Cannot get unexisting component {!r} from {}.' 118 | .format(comp_type.__name__, self)) 119 | 120 | return self._components[comp_type] 121 | 122 | def has(self, *args): 123 | """Checks if the entity has all components of the given type(s). 124 | :param args: namedtuple types 125 | :rtype: bool 126 | """ 127 | if len(args) == 1: 128 | return args[0] in self._components 129 | 130 | return all([comp_type in self._components for comp_type in args]) 131 | 132 | def has_any(self, *args): 133 | """Checks if the entity has any component of the given type(s). 134 | :param args: namedtuple types 135 | :rtype: bool 136 | """ 137 | return any([comp_type in self._components for comp_type in args]) 138 | 139 | def remove_all(self): 140 | """Removes all components.""" 141 | for comp_type in list(self._components): 142 | self._replace(comp_type, None) 143 | 144 | def destroy(self): 145 | """This method is used internally. Don't call it yourself. 146 | Use context.destroy_entity(entity). 147 | """ 148 | self._is_enabled = False 149 | self.remove_all() 150 | 151 | def __repr__(self): 152 | """ """ 153 | return ''.format( 154 | self._creation_index, 155 | ', '.join([str(self._components[x]) for x in self._components])) 156 | --------------------------------------------------------------------------------