Skip to content

Commit

Permalink
CachedWorld is has now replaced World. Caching by default.
Browse files Browse the repository at this point in the history
  • Loading branch information
Benjamin Moran committed Jun 18, 2018
1 parent 8e8bb4b commit fdd898d
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 350 deletions.
98 changes: 35 additions & 63 deletions esper.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,19 @@ def __init__(self, timed=False):
self._dead_entities = set()
if timed:
self.process_times = {}
self.process = self._timed_process
self._process = self._timed_process

def clear_cache(self):
self.get_component.cache_clear()
self.get_components.cache_clear()

def clear_database(self):
"""Remove all Entities and Components from the World."""
self._next_entity_id = 0
self._dead_entities.clear()
self._components.clear()
self._entities.clear()
self.clear_cache()

def add_processor(self, processor_instance, priority=0):
"""Add a Processor instance to the World.
Expand Down Expand Up @@ -92,9 +97,11 @@ def create_entity(self, *components):
"""
self._next_entity_id += 1

# TODO: duplicate add_component code here for performance
for component in components:
self.add_component(self._next_entity_id, component)

# self.clear_cache()
return self._next_entity_id

def delete_entity(self, entity, immediate=False):
Expand All @@ -110,16 +117,15 @@ def delete_entity(self, entity, immediate=False):
:param entity: The Entity ID you wish to delete.
:param immediate: If True, delete the Entity immediately.
"""

if immediate:
# If you modify this, make sure you reflect changes in _clear_dead_entities
for component_type in self._entities[entity]:
self._components[component_type].discard(entity)

if not self._components[component_type]:
del self._components[component_type]

del self._entities[entity]
self.clear_cache()

else:
self._dead_entities.add(entity)
Expand Down Expand Up @@ -184,6 +190,7 @@ def add_component(self, entity, component_instance):
self._entities[entity] = {}

self._entities[entity][component_type] = component_instance
self.clear_cache()

def remove_component(self, entity, component_type):
"""Remove a Component instance from an Entity, by type.
Expand All @@ -207,19 +214,21 @@ def remove_component(self, entity, component_type):
if not self._entities[entity]:
del self._entities[entity]

self.clear_cache()
return entity

def get_component(self, component_type):
def _get_component(self, component_type):
"""Get an iterator for Entity, Component pairs.
:param component_type: The Component type to retrieve.
:return: An iterator for (Entity, Component) tuples.
"""
entity_db = self._entities

for entity in self._components.get(component_type, []):
yield entity, entity_db[entity][component_type]

def get_components(self, *component_types):
def _get_components(self, *component_types):
"""Get an iterator for Entity and multiple Component sets.
:param component_types: Two or more Component types.
Expand All @@ -235,17 +244,25 @@ def get_components(self, *component_types):
except KeyError:
pass

@_lru_cache()
def get_component(self, component_type):
return [query for query in self._get_component(component_type)]

@_lru_cache()
def get_components(self, *component_types):
return [query for query in self._get_components(*component_types)]

def try_component(self, entity, component_type):
"""Try to get a single component type for an Entity.
This method will return the requested Component if it exists, but
will pass silently if it does not. This allows a way to access optional
Components that may not exist.
Components that may or may not exist.
:param entity: The Entity ID to retrieve the Component for.
:param component_type: The Component instance you wish to retrieve.
:return: A generator, containg the single Component instance requested,
or pass silently if it doesn't exist.
:return: A iterator containg the single Component instance requested,
which is empty if the component doesn't exist.
"""
if component_type in self._entities[entity]:
yield self._entities[entity][component_type]
Expand All @@ -270,11 +287,20 @@ def _clear_dead_entities(self):
del self._entities[entity]

self._dead_entities.clear()
self.clear_cache()

def _process(self, *args):
for processor in self._processors:
processor.process(*args)

def _timed_process(self, *args):
"""Track Processor execution time for benchmarking."""
for processor in self._processors:
start_time = _time.process_time()
processor.process(*args)
process_time = int(round((_time.process_time() - start_time) * 1000, 2))
self.process_times[processor.__class__.__name__] = process_time

def process(self, *args):
"""Call the process method on all Processors, in order of their priority.
Expand All @@ -289,59 +315,5 @@ def process(self, *args):
self._clear_dead_entities()
self._process(*args)

def _timed_process(self, *args):
"""Track Processor execution time for benchmarking."""
self._clear_dead_entities()
for processor in self._processors:
start_time = _time.process_time()
processor.process(*args)
process_time = int(round((_time.process_time() - start_time) * 1000, 2))
self.process_times[processor.__class__.__name__] = process_time


class CachedWorld(World):
def clear_cache(self):
self.get_component.cache_clear()
self.get_components.cache_clear()

def clear_database(self):
super().clear_database()
self.clear_cache()

def create_entity(self, *components):
self.clear_cache()
return super().create_entity(*components)

def delete_entity(self, entity, immediate=False):
"""Delete an Entity from the World.
Delete an Entity and all of it's assigned Component instances from
the world. By default, Entity deletion is delayed until the next call
to *World.process*. You can request immediate deletion, however, by
passing the "immediate=True" parameter. This should generally not be
done during Entity iteration (calls to World.get_component/s).
Raises a KeyError if the given entity does not exist in the database.
:param entity: The Entity ID you wish to delete.
:param immediate: If True, delete the Entity immediately.
"""
super().delete_entity(entity, immediate)
if immediate:
self.clear_cache()

def add_component(self, entity, component_instance):
super().add_component(entity, component_instance)
self.clear_cache()

def remove_component(self, entity, component_type):
super().remove_component(entity, component_type)
self.clear_cache()
return entity

@_lru_cache()
def get_component(self, component_type):
return [query for query in super().get_component(component_type)]

@_lru_cache()
def get_components(self, *component_types):
return [query for query in super().get_components(*component_types)]
CachedWorld = World
20 changes: 3 additions & 17 deletions examples/benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
# Commandline options:
######################
parser = optparse.OptionParser()
parser.add_option("-c", "--cached", dest="cached", action="store_true", default=False,
help="Benchmark esper.CachedWorld instead of esper.World.")
parser.add_option("-p", "--plot", dest="plot", action="store_true", default=False,
help="Display benchmark. Requires matplotlib module.")
parser.add_option("-w", "--walltime", dest="walltime", action="store_true", default=False,
Expand Down Expand Up @@ -52,11 +50,7 @@ def wrap(*args):
##############################
# Instantiate the game world:
##############################
if options.cached:
print("Benchmarking CachedWorld...\n")
world = esper.CachedWorld()
else:
world = esper.World()
world = esper.World()


#################################
Expand Down Expand Up @@ -106,16 +100,8 @@ def __init__(self):
#############################
def create_entities(number):
for _ in range(number // 2):
enemy = world.create_entity()
world.add_component(enemy, Position())
world.add_component(enemy, Velocity())
world.add_component(enemy, Health())
world.add_component(enemy, Command())

thing = world.create_entity()
world.add_component(thing, Position())
world.add_component(thing, Health())
world.add_component(thing, Damageable())
world.create_entity(Position(), Velocity(), Health(), Command())
world.create_entity(Position(), Health(), Damageable())


#############################
Expand Down
26 changes: 11 additions & 15 deletions examples/benchmark_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ def wrap(*args):
return ret
return wrap


##########################
# Create a World instance:
##########################
world = esper.CachedWorld()
world = esper.World()


#################################
Expand Down Expand Up @@ -106,16 +107,8 @@ def process(self):
#############################
def create_entities(world, number):
for _ in range(number // 2):
enemy = world.create_entity()
world.add_component(enemy, Position())
world.add_component(enemy, Velocity())
world.add_component(enemy, Health())
world.add_component(enemy, Command())

thing = world.create_entity()
world.add_component(thing, Position())
world.add_component(thing, Health())
world.add_component(thing, Damageable())
world.create_entity(Position(), Velocity(), Health(), Command())
world.create_entity(Position(), Health(), Damageable())


#################################################
Expand All @@ -138,13 +131,16 @@ def query_entities(world):
for current_pass in range(10):
world.clear_database()
create_entities(world, MAX_ENTITIES)
print("Pass {}...".format(current_pass + 1))

print(f"Pass {current_pass + 1}...")

for amount in range(1, 500):
query_entities(world)
if amount > 250 and amount < 400:
world.delete_entity(amount)

if amount > 250:
world.delete_entity(amount, immediate=True)
create_entities(world, 1)
world.process()

results.append(current_run)
current_run = []

Expand Down
Loading

0 comments on commit fdd898d

Please sign in to comment.