-
-
Notifications
You must be signed in to change notification settings - Fork 95
/
Copy pathengine.py
211 lines (177 loc) · 6.55 KB
/
engine.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import time
from collections import defaultdict
from collections import deque
from contextlib import ExitStack
from itertools import chain
from typing import Any
from typing import Callable
from typing import DefaultDict
from typing import List
from typing import Type
from typing import Union
import ppb.events as events
from ppb.abc import Engine
from ppb.events import EventMixin
from ppb.events import Quit
from ppb.events import StartScene
from ppb.systems import PygameEventPoller
from ppb.systems import Renderer
from ppb.systems import Updater
from ppb.utils import LoggingMixin
_ellipsis = type(...)
class GameEngine(Engine, EventMixin, LoggingMixin):
def __init__(self, first_scene: Type, *,
systems=(Renderer, Updater, PygameEventPoller),
scene_kwargs=None, **kwargs):
super(GameEngine, self).__init__()
# Engine Configuration
self.first_scene = first_scene
self.scene_kwargs = scene_kwargs or {}
self.kwargs = kwargs
# Engine State
self.scenes = []
self.events = deque()
self.event_extensions: DefaultDict[Union[Type, _ellipsis],
List[Callable[[Any], None]]] = defaultdict(list)
self.running = False
self.entered = False
self._last_idle_time = None
# Systems
self.systems_classes = systems
self.systems = []
self.exit_stack = ExitStack()
@property
def current_scene(self):
try:
return self.scenes[-1]
except IndexError:
return None
def __enter__(self):
self.logger.info("Entering context")
self.start_systems()
self.entered = True
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.logger.info("Exiting context")
self.entered = False
self.exit_stack.close()
def start_systems(self):
if self.systems:
return
for system in self.systems_classes:
if isinstance(system, type):
system = system(engine=self, **self.kwargs)
self.systems.append(system)
self.exit_stack.enter_context(system)
def run(self):
if not self.entered:
with self:
self.start()
self.main_loop()
else:
self.start()
self.main_loop()
def start(self):
self.running = True
self._last_idle_time = time.monotonic()
self.activate({"scene_class": self.first_scene,
"kwargs": self.scene_kwargs})
def main_loop(self):
while self.running:
time.sleep(0)
self.loop_once()
def loop_once(self):
if not self.entered:
raise ValueError("Cannot run before things have started",
self.entered)
now = time.monotonic()
self.signal(events.Idle(now - self._last_idle_time))
self._last_idle_time = now
while self.events:
self.publish()
def activate(self, next_scene: dict):
scene = next_scene["scene_class"]
if scene is None:
return
args = next_scene.get("args", [])
kwargs = next_scene.get("kwargs", {})
self.scenes.append(scene(self, *args, **kwargs))
def signal(self, event):
self.events.append(event)
def publish(self):
event = self.events.popleft()
scene = self.current_scene
event.scene = scene
extensions = chain(self.event_extensions[type(event)], self.event_extensions[...])
for callback in extensions:
callback(event)
self.__event__(event, self.signal)
for system in self.systems:
system.__event__(event, self.signal)
# Required for if we publish with no current scene.
# Should only happen when the last scene stops via event.
if scene is not None:
scene.__event__(event, self.signal)
for game_object in scene:
game_object.__event__(event, self.signal)
def on_start_scene(self, event: StartScene, signal: Callable[[Any], None]):
"""
Start a new scene. The current scene pauses.
"""
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.
"""
self.stop_scene()
if self.current_scene is not None:
signal(events.SceneContinued())
else:
signal(events.Quit())
def on_replace_scene(self, event: events.ReplaceScene, signal):
"""
Replace the running scene with a new one.
"""
self.stop_scene()
self.start_scene(event.new_scene, event.kwargs)
def on_quit(self, quit_event: Quit, signal: Callable[[Any], None]):
self.running = False
def pause_scene(self):
# Empty the queue before changing scenes.
self.flush_events()
self.signal(events.ScenePaused())
self.publish()
def stop_scene(self):
# Empty the queue before changing scenes.
self.flush_events()
self.signal(events.SceneStopped())
self.publish()
self.scenes.pop()
def start_scene(self, scene, kwargs):
if isinstance(scene, type):
scene = scene(self, **(kwargs or {}))
self.scenes.append(scene)
self.signal(events.SceneStarted())
def register(self, event_type: Union[Type, _ellipsis], callback: Callable[[], Any]):
"""
Register a callback to be applied to an event at time of publishing.
Primarily to be used by subsystems.
The callback will receive the event. Your code should modify the event
in place. It does not need to return it.
:param event_type: The class of an event.
:param callback: A callable, must accept an event, and return no value.
:return: None
"""
if not isinstance(event_type, type) and event_type is not ...:
raise TypeError(f"{type(self)}.register requires event_type to be a type.")
if not callable(callback):
raise TypeError(f"{type(self)}.register requires callback to be callable.")
self.event_extensions[event_type].append(callback)
def flush_events(self):
"""
Flush the event queue.
Call before doing anything that will cause signals to be delivered to
the wrong scene.
"""
self.events = deque()