"""
A component-based entity system framework.
ebs loosely follows a component oriented pattern to separate object
instances, carried data and processing logic within applications or
games. It uses a entity based approach, in which object instances are
unique identifiers, while their data is managed within components, which
are separately stored. For each individual component type a processing
system will take care of all necessary updates for the World
environment.
"""
import uuid
import inspect
from .compat import *
__all__ = ["Entity", "World", "System", "Applicator"]
[docs]class Entity(object):
"""A simple object entity.
An entity is a specific object living in the application world. It
does not carry any data or application logic, but merely acts as
identifier label for data that is maintained in the application
world itself.
As such, it is an composition of components, which would not exist
without the entity identifier. The entity itself is non-existent to
the application world as long as it does not carry any data that can
be processed by a system within the application world.
"""
def __new__(cls, world, *args, **kwargs):
if not isinstance(world, World):
raise TypeError("world must be a World")
entity = object.__new__(cls)
entity._id = uuid.uuid4()
entity._world = world
world.entities.add(entity)
return entity
def __repr__(self):
return "Entity(id=%s)" % self._id
def __hash__(self):
return hash(self._id)
def __getattr__(self, name):
"""Gets the component data related to the Entity."""
if name in ("_id", "_world"):
return object.__getattribute__(self, name)
try:
ctype = self._world._componenttypes[name]
except KeyError:
raise AttributeError("object '%r' has no attribute '%r'" % \
(self.__class__.__name__, name))
return self._world.components[ctype][self]
def __setattr__(self, name, value):
"""Sets the component data related to the Entity."""
if name in ("_id", "_world"):
object.__setattr__(self, name, value)
else:
# If the value is a compound component (e.g. a Button
# inheriting from a Sprite), it needs to be added to all
# supported component type instances.
mro = inspect.getmro(value.__class__)
if type in mro:
stop = mro.index(type)
else:
stop = mro.index(object)
mro = mro[0:stop]
wctypes = self._world.componenttypes
for clstype in mro:
if clstype not in wctypes:
self._world.add_componenttype(clstype)
self._world.components[clstype][self] = value
def __delattr__(self, name):
"""Deletes the component data related to the Entity."""
if name in ("_id", "_world"):
raise AttributeError("'%s' cannot be deleted.", name)
try:
ctype = self._world._componenttypes[name]
except KeyError:
raise AttributeError("object '%s' has no attribute '%s'" % \
(self.__class__.__name__, name))
del self._world.components[ctype][self]
[docs] def delete(self):
"""Removes the Entity from the world it belongs to."""
self.world.delete(self)
@property
def id(self):
"""The id of the Entity."""
return self._id
@property
def world(self):
"""The world the Entity resides in."""
return self._world
[docs]class World(object):
"""A simple application world.
An application world defines the combination of application data and
processing logic and how the data will be processed. As such, it is
a container object in which the application is defined.
The application world maintains a set of entities and their related
components as well as a set of systems that process the data of the
entities. Each processing system within the application world only
operates on a certain set of components, but not all components of
an entity at once.
The order in which data is processed depends on the order of the
added systems.
"""
def __init__(self):
"""Creates a new World instance."""
self.entities = set()
self._systems = []
self.components = {}
self._componenttypes = {}
def _system_is_valid(self, system):
"""Checks, if the passed object fulfills the requirements for being
a processing system.
"""
return hasattr(system, "componenttypes") and \
isiterable(system.componenttypes) and \
hasattr(system, "process") and \
callable(system.process)
def combined_components(self, comptypes):
"""A generator view on combined sets of component items."""
comps = self.components
keysets = [set(comps[ctype]) for ctype in comptypes]
valsets = [comps[ctype] for ctype in comptypes]
entities = keysets[0].intersection(*keysets[1:])
for ekey in entities:
yield tuple(component[ekey] for component in valsets)
def add_componenttype(self, classtype):
"""Adds a supported component type to the World."""
if classtype in self._componenttypes.values():
return
self.components[classtype] = {}
self._componenttypes[classtype.__name__.lower()] = classtype
[docs] def delete(self, entity):
"""Removes an Entity from the World, including all its data."""
for componentset in self.components.values():
componentset.pop(entity, None)
self.entities.discard(entity)
[docs] def delete_entities(self, entities):
"""Removes multiple entities from the World at once."""
eids = set(entities)
if ISPYTHON2:
for compkey, compset in self.components.viewitems():
keys = set(compset.viewkeys()) - eids
self.components[compkey] = dict((k, compset[k]) for k in keys)
else:
for compkey, compset in self.components.items():
keys = set(compset.keys()) - eids
self.components[compkey] = dict((k, compset[k]) for k in keys)
self.entities -= set(entities)
def get_components(self, componenttype):
"""Gets all existing components for a sepcific component type.
If no components could be found for the passed component types, an
empty list is returned.
"""
if componenttype in self.components:
return self.components[componenttype].values()
return []
[docs] def get_entities(self, component):
"""Gets the entities using the passed component.
Note: this will not perform an identity check on the component
but rely on its __eq__ implementation instead.
"""
compset = self.components.get(component.__class__, None)
if compset is None:
return []
return [e for e in compset if compset[e] == component]
[docs] def add_system(self, system):
"""Adds a processing system to the world.
The system will be added as last item in the processing order. Every
object can be added as long as it contains
* a 'componenttypes' attribute that is iterable and contains the
class types to be processed
* a 'process()' method, receiving two arguments, the world and
components
If the object contains a 'is_applicator' attribute that evaluates to
True, the system will operate on combined sets of components.
"""
if not self._system_is_valid(system):
raise ValueError("system must have componenttypes and a process method")
for classtype in system.componenttypes:
if classtype not in self.components:
self.add_componenttype(classtype)
self._systems.append(system)
[docs] def insert_system(self, index, system):
"""Adds a processing system to the world.
The system will be added at the specific position of the
processing order.
"""
if not self._system_is_valid(system):
raise ValueError("system must have componenttypes and a process method")
for classtype in system.componenttypes:
if classtype not in self.components:
self.add_componenttype(classtype)
self._systems.insert(index, system)
[docs] def remove_system(self, system):
"""Removes a processing system from the world."""
self._systems.remove(system)
[docs] def process(self):
"""Processes all components within their corresponding systems."""
components = self.components
for system in self._systems:
s_process = system.process
if getattr(system, "is_applicator", False):
comps = self.combined_components(system.componenttypes)
s_process(self, comps)
else:
if ISPYTHON2:
for ctype in system.componenttypes:
s_process(self, components[ctype].viewvalues())
else:
for ctype in system.componenttypes:
s_process(self, components[ctype].values())
@property
def systems(self):
"""Gets the systems bound to the world."""
return tuple(self._systems)
@property
def componenttypes(self):
"""Gets the supported component types of the world."""
return self._componenttypes.values()
[docs]class System(object):
"""A processing system for component data.
A processing system within an application world consumes the
components of all entities, for which it was set up. At time of
processing, the system does not know about any other component type
that might be bound to any entity.
Also, the processing system does not know about any specific entity,
but only is aware of the data carried by all entities.
"""
def __init__(self):
self.componenttypes = None
[docs] def process(self, world, components):
"""Processes component items.
This must be implemented by inheriting classes.
"""
raise NotImplementedError()
[docs]class Applicator(System):
"""A processing system for combined data sets."""
def __init__(self):
super(Applicator, self).__init__()
self.is_applicator = True