Skip to content

Commit

Permalink
Merge #423
Browse files Browse the repository at this point in the history
423: Adds docs for GameEngine r=astronouth7303 a=pathunstrom



Co-authored-by: Piper Thunstrom <pathunstrom@gmail.com>
  • Loading branch information
bors[bot] and pathunstrom authored Apr 15, 2020
2 parents 1d4f4f0 + e165437 commit faaaeca
Show file tree
Hide file tree
Showing 5 changed files with 135 additions and 13 deletions.
1 change: 1 addition & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,4 @@

# -- autodoc configuration
autodoc_mock_imports = ['sdl2']
autoclass_content = 'both'
16 changes: 16 additions & 0 deletions docs/reference/engine.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.. py:currentmodule:: ppb.engine
================
GameEngine
================

The :py:class:`GameEngine` is the literal beating heart of ppb: It publishes the
event queue, is the source of the :class:`~events.Idle` event, and is the
root container of the object tree.

Some of the engine of the API is definitely intended for advanced users. Use
the various methods of :py:class:`GameEngine` with caution.

.. autoclass:: ppb.GameEngine
:members:
:undoc-members:
1 change: 1 addition & 0 deletions docs/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ decisions are made, see the discussion section.
assets
scenes
sprites
engine
sound
features/index
128 changes: 116 additions & 12 deletions ppb/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,35 @@ def _get_handler_name(txt):


class GameEngine(LoggingMixin):
"""
The core component of :mod:`ppb`.
To use the engine directly, treat it as a context manager: ::
with GameEngine(BaseScene, **kwargs) as ge:
ge.run()
"""
def __init__(self, first_scene: Type, *,
basic_systems=(Renderer, Updater, EventPoller, SoundController, AssetLoadingSystem),
systems=(), scene_kwargs=None, **kwargs):
"""
:param first_scene: A :class:`~ppb.BaseScene` type.
:type first_scene: Type
:param basic_systems: :class:systemslib.Systems that are considered
the "default". Includes: :class:`~systems.Renderer`,
:class:`~systems.Updater`, :class:`~systems.EventPoller`,
:class:`~systems.SoundController`, :class:`~systems.AssetLoadingSystem`.
:type basic_systems: Iterable[systemslib.System]
:param systems: Additional user defined systems.
:type systems: Iterable[systemslib.System]
:param scene_kwargs: Keyword arguments passed along to the first scene.
:type scene_kwargs: Dict[str, Any]
:param kwargs: Additional keyword arguments. Passed to the systems.
.. warning::
Passing in your own ``basic_systems`` can have unintended
consequences. Consider passing via systems parameter instead.
"""

super(GameEngine, self).__init__()

Expand All @@ -65,6 +90,12 @@ def __init__(self, first_scene: Type, *,

@property
def current_scene(self):
"""
The top of the scene stack.
:return: The currently running scene.
:rtype: ppb.BaseScene
"""
try:
return self.scenes[-1]
except IndexError:
Expand All @@ -82,6 +113,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
self.exit_stack.close()

def start_systems(self):
"""Initialize and enter the systems."""
if self.systems:
return
for system in self.systems_classes:
Expand All @@ -91,6 +123,16 @@ def start_systems(self):
self.exit_stack.enter_context(system)

def run(self):
"""
Begin the main loop.
If you have not entered the :class:`GameEngine`, this function will
enter it for you before starting.
Example: ::
GameEngine(BaseScene, **kwargs).run()
"""
if not self.entered:
with self:
self.start()
Expand All @@ -100,17 +142,37 @@ def run(self):
self.main_loop()

def start(self):
"""
Starts the engine.
Called by :meth:`GameEngine.run` before :meth:`GameEngine.main_loop`.
You shouldn't call this yourself unless you're embedding :mod:`ppb` in
another event loop.
"""
self.running = True
self._last_idle_time = time.monotonic()
self.activate({"scene_class": self.first_scene,
"kwargs": self.scene_kwargs})

def main_loop(self):
"""
Loop forever.
If you're embedding :mod:`ppb` in an external event loop you should not
use this method. Call :meth:`GameEngine.loop_once` instead.
"""
while self.running:
time.sleep(0)
self.loop_once()

def loop_once(self):
"""
Iterate once.
If you're embedding :mod:`ppb` in an external event loop call once per
loop.
"""
if not self.entered:
raise ValueError("Cannot run before things have started",
self.entered)
Expand All @@ -121,22 +183,38 @@ def loop_once(self):
self.publish()

def activate(self, next_scene: dict):
"""
Instantiates and sets up a new scene.
:param next_scene: A dictionary with the keys:
* "scene_class": A :class:`~ppb.BaseScene` type.
* "args": A :class:`list` of positional arguments.
* "kwargs": A :class:`dict` of keyword arguments.
"""
scene = next_scene["scene_class"]
if scene is None:
return
args = next_scene.get("args", [])
kwargs = next_scene.get("kwargs", {})
self.start_scene(scene(*args, **kwargs), None)
self._start_scene(scene(*args, **kwargs), None)

def signal(self, event):
"""
Add an event to the event queue.
Thread-safe.
You will rarely call this directly from a :class:`GameEngine` instance.
The current :class:`GameEngine` instance will pass it's signal method
as part of publishing an event.
"""
self.events.append(event)

def publish(self):
"""
Publish the next event to every object in the tree.
"""
event = self.events.popleft()
scene = self.current_scene
event.scene = scene
Expand Down Expand Up @@ -165,15 +243,21 @@ def publish(self):
def on_start_scene(self, event: events.StartScene, signal: Callable[[Any], None]):
"""
Start a new scene. The current scene pauses.
Do not call this method directly. It is called by the GameEngine when a
:class:`~events.StartScene` event is fired.
"""
self.pause_scene()
self.start_scene(event.new_scene, event.kwargs)
self._pause_scene()
self._start_scene(event.new_scene, event.kwargs)

def on_stop_scene(self, event: events.StopScene, signal: Callable[[Any], None]):
"""
Stop a running scene. If there's a scene on the stack, it resumes.
Do not call this method directly. It is called by the GameEngine when a
:class:`~events.StopScene` event is fired.
"""
self.stop_scene()
self._stop_scene()
if self.current_scene is not None:
signal(events.SceneContinued())
else:
Expand All @@ -182,27 +266,39 @@ def on_stop_scene(self, event: events.StopScene, signal: Callable[[Any], None]):
def on_replace_scene(self, event: events.ReplaceScene, signal):
"""
Replace the running scene with a new one.
Do not call this method directly. It is called by the GameEngine when a
:class:`~events.ReplaceScene` event is fired.
"""
self.stop_scene()
self.start_scene(event.new_scene, event.kwargs)
self._stop_scene()
self._start_scene(event.new_scene, event.kwargs)

def on_quit(self, quit_event: events.Quit, signal: Callable[[Any], None]):
"""
Shut down the event loop.
Do not call this method directly. It is called by the GameEngine when a
:class:`~events.Quit` event is fired.
"""
self.running = False

def pause_scene(self):
def _pause_scene(self):
"""Pause the current scene."""
# Empty the queue before changing scenes.
self.flush_events()
self._flush_events()
self.signal(events.ScenePaused())
self.publish()

def stop_scene(self):
def _stop_scene(self):
"""Stop the current scene."""
# Empty the queue before changing scenes.
self.flush_events()
self._flush_events()
self.signal(events.SceneStopped())
self.publish()
self.scenes.pop()

def start_scene(self, scene, kwargs):
def _start_scene(self, scene, kwargs):
"""Start a scene."""
if isinstance(scene, type):
scene = scene(**(kwargs or {}))
self.scenes.append(scene)
Expand All @@ -227,7 +323,7 @@ def register(self, event_type: Union[Type, _ellipsis], callback: Callable[[], An
raise TypeError(f"{type(self)}.register requires callback to be callable.")
self.event_extensions[event_type].append(callback)

def flush_events(self):
def _flush_events(self):
"""
Flush the event queue.
Expand All @@ -237,6 +333,14 @@ def flush_events(self):
self.events = deque()

def walk(self):
"""
Walk the object tree.
Publication order: The :class:`GameEngine`, the
:class:`~ppb.systemslib.System` list, the current
:class:`~ppb.BaseScene`, then finally the :class:`~ppb.Sprite` objects
in the current scene.
"""
yield self
yield from self.systems
yield self.current_scene
Expand Down
2 changes: 1 addition & 1 deletion tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def test_flush_events():

assert len(ge.events) == 2

ge.flush_events()
ge._flush_events()

assert len(ge.events) == 0

Expand Down

0 comments on commit faaaeca

Please sign in to comment.