diff --git a/README.md b/README.md index c1ec444..38979bd 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,8 @@ Design * World Context -Esper uses the concept of "World" contexts. When you import esper, a default context is active. -You create Entities, assign Components, register Processesors, etc., by calling functions +Esper uses the concept of "World" contexts. When you first `import esper`, a default context is +active. You create Entities, assign Components, register Processesors, etc., by calling functions on the `esper` module. Entities, Components and Processors can be created, assigned, or deleted while your game is running. A simple call to `esper.process()` is all that's needed for each iteration of your game loop. Advanced users can switch contexts, which can be useful for @@ -184,19 +184,20 @@ General Usage World Contexts -------------- Esper has the capability of supporting multiple "World" contexts. On import, a "default" World is -active. All creation of Entities, assignment of Processors, and all operations exist within the -confines of a World. For advanced use cases Esper allows you to switch between multiple Worlds, -which are completely isolated from each other. This can be useful when different scenes in your -game have different Entities and Processor requirements. World context operations are done with -the following functions:: - +active. All creation of Entities, assignment of Processors, and all other operations occur within +the confines of the active World. In other words, the World contexts are completely isolated from +each other. For basic games and designs, you may not need to bother with this functionality. A +single default World context can often be enough. For advanced use cases, such as when different +scenes in your game have different Entities and Processor requirements, this functionality can be +quite useful. World context operations are done with the following functions:: +* * esper.list_worlds() * esper.switch_world(name) * esper.delete_world(name) -When switching Worlds, be careful of the `name`. If a World doesn't exist, it will be created. -You can delete old Worlds which are no longer needed, but you cannot delete the currently active -World. +When switching Worlds, be mindful of the `name`. If a World doesn't exist, it will be created when +you first switch to it. You can delete old Worlds if they are no longer needed, but you can not +delete the currently active World. Adding and Removing Processors ------------------------------ @@ -390,8 +391,8 @@ Contributions to Esper are always welcome, but there are some specific project g - Pure Python code only: no binary extensions, Cython, etc. - Try to target all non-EOL Python versions. Exceptions can be made if there is a compelling reason. -- Avoid bloat as much as possible. New features will be considered if they are commonly useful. Generally speaking, we don't want to add functionality that is better handled in another module or library. -- Performance is preferrable to readability. +- Avoid bloat as much as possible. New features will be considered if they are commonly useful. Generally speaking, we don't want to add functionality that is better served by another module or library. +- Performance is preferrable to readability. The public API should remain clean, but ugly internal code is acceptable if it provides a performance benefit. Every cycle counts! If you have any questions before contributing, feel free to [open an issue]. diff --git a/RELEASE_NOTES b/RELEASE_NOTES index 68ba9fb..0218441 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -1,3 +1,13 @@ +esper 3.2 +========= +Maintenance release + +Changes +------- +- Add `esper.current_world` property to easily check the current World context. +- Made some minor docstring corrections, and added some programmer notes. + + esper 3.1 ========= Maintenance release diff --git a/esper/__init__.py b/esper/__init__.py index 3c02679..15caa1c 100644 --- a/esper/__init__.py +++ b/esper/__init__.py @@ -24,7 +24,7 @@ from itertools import count as _count -version = '3.1' +version = '3.2' __version__ = version @@ -106,23 +106,19 @@ class Processor: Processor instances must contain a `process` method, but you are otherwise free to define the class any way you wish. Processors should be instantiated, - and then added to a :py:class:`esper.World` instance by calling - :py:func:`esper.World.add_processor`. For example:: - - my_world = World() + and then added to the current World context by calling :py:func:`esper.add_processor`. + For example:: my_processor_instance = MyProcessor() - my_world.add_processor(my_processor_instance) + esper.add_processor(my_processor_instance) - After adding your Processors to a :py:class:`esper.World`, Processor.world - will be set to the World it is in. This allows easy access to the World and - it's methods from your Processor methods. All Processors in a World will have - their `process` methods called by a single call to :py:func:`esper.World.process`, - so you will generally want to iterate over entities with one (or more) calls to - the appropriate world methods:: + All the Processors that have been added to the World context will have their + :py:meth:`esper.Processor.process` methods called by a single call to + :py:func:`esper.process`. Inside the `process` method is generally where you + should iterate over Entities with one (or more) calls to the appropriate methods:: def process(self): - for ent, (rend, vel) in self.world.get_components(Renderable, Velocity): + for ent, (rend, vel) in esper.get_components(Renderable, Velocity): your_code_here() """ @@ -506,9 +502,9 @@ def process(*args: _Any, **kwargs: _Any) -> None: def timed_process(*args: _Any, **kwargs: _Any) -> None: """Track Processor execution time for benchmarking. - This function is identical to :py:func:`esper.process`, - but it will additionally record the elapsed time of each - processor call in the `esper.process_times` dictionary. + This function is identical to :py:func:`esper.process`, but + it additionally records the elapsed time of each processor + call (in milliseconds) in the`esper.process_times` dictionary. """ clear_dead_entities() for processor in _processors: @@ -554,7 +550,7 @@ def switch_world(name: str) -> None: already active. """ if name not in _context_map: - # Create a new + # Create a new context if the name does not already exist: _context_map[name] = (_count(start=1), {}, {}, set(), {}, {}, [], {}, {}) global _current_context @@ -568,6 +564,16 @@ def switch_world(name: str) -> None: global process_times global event_registry + # switch the references to the objects in the named context_map: (_entity_count, _components, _entities, _dead_entities, _get_component_cache, _get_components_cache, _processors, process_times, event_registry) = _context_map[name] _current_context = name + + +@property +def current_world() -> str: + """The currently active World context. + + To switch World contexts, see :py:func:`esper.switch_world`. + """ + return _current_context \ No newline at end of file diff --git a/tests/test_world.py b/tests/test_world.py index ac9fcd9..3f832cd 100644 --- a/tests/test_world.py +++ b/tests/test_world.py @@ -275,14 +275,24 @@ def test_clear_dead_entities(): def test_switch_world(): + # The `create_entities` helper will add /2 of + # 'ComponentA' to the World context. Make a new + # "left" context, and confirm this is True: esper.switch_world("left") assert len(esper.get_component(ComponentA)) == 0 create_entities(200) assert len(esper.get_component(ComponentA)) == 100 + + # Switching to a new "right" World context, no + # 'ComponentA' Components should yet exist. esper.switch_world("right") assert len(esper.get_component(ComponentA)) == 0 create_entities(300) assert len(esper.get_component(ComponentA)) == 150 + + # Switching back to the original "left" context, + # the original 100 Components should still exist. + # From there, 200 more should be added: esper.switch_world("left") assert len(esper.get_component(ComponentA)) == 100 create_entities(400) @@ -293,6 +303,16 @@ def test_switch_world(): # Some helper functions and Component templates: ################################################## def create_entities(number): + """This function will create X number of entities. + + The entities are created with a mix of Components, + so the World context will see an addition of + ComponentA * number * 1 + ComponentB * number * 1 + ComponentC * number * 2 + ComponentD * number * 1 + ComponentE * number * 1 + """ for _ in range(number // 2): esper.create_entity(ComponentA(), ComponentB(), ComponentC()) esper.create_entity(ComponentC(), ComponentD(), ComponentE()) @@ -459,17 +479,29 @@ def test_event_handler_switch_world(): def handler(): nonlocal called called += 1 + + # Switch to a new "left" World context, and register + # an event handler. Confirm that it is being called + # by checking that the 'called' variable is incremented. esper.switch_world("left") esper.set_handler("foo", handler) assert called == 0 esper.dispatch_event("foo") assert called == 1 + + # Here we switch to a new "right" World context. + # The handler is registered to the "left" context only, + # so dispatching the event should have no effect. The + # handler is not attached, and so the 'called' value + # should not be incremented further. esper.switch_world("right") - assert called == 1 esper.dispatch_event("foo") assert called == 1 + + # Switching back to the "left" context and dispatching + # the event, the handler should still be registered and + # the 'called' variable should be incremented by 1. esper.switch_world("left") - assert called == 1 esper.dispatch_event("foo") assert called == 2