pgzero-plus 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pgzero-plus might be problematic. Click here for more details.
- pgzero/__init__.py +41 -0
- pgzero/__main__.py +3 -0
- pgzero/actor.py +597 -0
- pgzero/animation.py +244 -0
- pgzero/builtins.py +45 -0
- pgzero/clock.py +200 -0
- pgzero/constants.py +33 -0
- pgzero/data/icon.png +0 -0
- pgzero/data/joypad.png +0 -0
- pgzero/game.py +573 -0
- pgzero/keyboard.py +67 -0
- pgzero/loaders.py +308 -0
- pgzero/music.py +180 -0
- pgzero/ptext.py +500 -0
- pgzero/rect.py +523 -0
- pgzero/runner.py +233 -0
- pgzero/screen.py +210 -0
- pgzero/soundfmt.py +105 -0
- pgzero/spellcheck.py +173 -0
- pgzero/sprites.py +182 -0
- pgzero/storage.py +191 -0
- pgzero/tilemaps.py +307 -0
- pgzero/tone.py +132 -0
- pgzero_plus-0.1.0.dist-info/METADATA +259 -0
- pgzero_plus-0.1.0.dist-info/RECORD +30 -0
- pgzero_plus-0.1.0.dist-info/WHEEL +5 -0
- pgzero_plus-0.1.0.dist-info/entry_points.txt +2 -0
- pgzero_plus-0.1.0.dist-info/licenses/COPYING +165 -0
- pgzero_plus-0.1.0.dist-info/top_level.txt +2 -0
- pgzrun.py +34 -0
pgzero/game.py
ADDED
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import operator
|
|
3
|
+
import time
|
|
4
|
+
import types
|
|
5
|
+
from time import perf_counter, sleep
|
|
6
|
+
|
|
7
|
+
import pygame
|
|
8
|
+
import pgzero.clock
|
|
9
|
+
import pgzero.keyboard
|
|
10
|
+
import pgzero.screen
|
|
11
|
+
import pgzero.loaders
|
|
12
|
+
import builtins
|
|
13
|
+
|
|
14
|
+
from . import constants
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
screen = None # This global surface is what actors draw to
|
|
18
|
+
DISPLAY_FLAGS = pygame.SHOWN
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Game:
|
|
22
|
+
"""
|
|
23
|
+
Simple game-wide state container for beginner projects.
|
|
24
|
+
|
|
25
|
+
Use this to keep track of things like score, level, timers, and
|
|
26
|
+
the current game state without needing lots of global variables.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self._start_callback = None
|
|
31
|
+
self._clock = None
|
|
32
|
+
self._is_restarting = False
|
|
33
|
+
self._restart_clear_mode = "immediate"
|
|
34
|
+
self.reset()
|
|
35
|
+
|
|
36
|
+
def _bind_engine(self, start_callback=None, clock=None):
|
|
37
|
+
self._start_callback = start_callback
|
|
38
|
+
self._clock = clock
|
|
39
|
+
|
|
40
|
+
def reset(self):
|
|
41
|
+
"""Reset the game back to its default starting values."""
|
|
42
|
+
self.state = "playing"
|
|
43
|
+
|
|
44
|
+
self.score = 0
|
|
45
|
+
self.level = 1
|
|
46
|
+
|
|
47
|
+
self.timer = 0
|
|
48
|
+
self.frame_count = 0
|
|
49
|
+
|
|
50
|
+
self.data = {}
|
|
51
|
+
|
|
52
|
+
def restart(self, delay=0.0, clear_clock="immediate"):
|
|
53
|
+
"""
|
|
54
|
+
Restart the game.
|
|
55
|
+
|
|
56
|
+
clear_clock options:
|
|
57
|
+
- "immediate": clear scheduled events right away
|
|
58
|
+
- "on_restart": clear scheduled events when the restart actually happens
|
|
59
|
+
- "never": do not clear scheduled events
|
|
60
|
+
|
|
61
|
+
Note: any previously scheduled restart is always unscheduled first.
|
|
62
|
+
"""
|
|
63
|
+
if self._is_restarting:
|
|
64
|
+
raise RuntimeError(
|
|
65
|
+
"game.restart() was called from inside start(). "
|
|
66
|
+
"Do not call game.restart() inside start(). "
|
|
67
|
+
"The library already calls start() during restart."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if clear_clock not in ("immediate", "on_restart", "never"):
|
|
71
|
+
raise ValueError("clear_clock must be 'immediate', 'on_restart', or 'never'")
|
|
72
|
+
|
|
73
|
+
self._check_can_delay(delay)
|
|
74
|
+
|
|
75
|
+
self._restart_clear_mode = clear_clock
|
|
76
|
+
|
|
77
|
+
if self._clock:
|
|
78
|
+
# Always cancel any pending restart first
|
|
79
|
+
self._clock.unschedule(self._do_restart)
|
|
80
|
+
|
|
81
|
+
# Only clear everything else immediately if requested
|
|
82
|
+
if clear_clock == "immediate":
|
|
83
|
+
self._clock.clear()
|
|
84
|
+
|
|
85
|
+
if delay and delay > 0:
|
|
86
|
+
self._clock.schedule_unique(self._do_restart, delay)
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
self._do_restart()
|
|
90
|
+
|
|
91
|
+
def _check_can_delay(self, delay):
|
|
92
|
+
"""Raise a helpful error if a delayed restart can't be scheduled.
|
|
93
|
+
|
|
94
|
+
Delays need the game clock, which is only connected once the game
|
|
95
|
+
starts running, so this happens when the call is at the top level
|
|
96
|
+
of the game file instead of inside a function.
|
|
97
|
+
"""
|
|
98
|
+
if delay and delay > 0 and self._clock is None:
|
|
99
|
+
raise RuntimeError(
|
|
100
|
+
"A delayed restart can only be used while the game is "
|
|
101
|
+
"running. Call it from inside a function such as update() "
|
|
102
|
+
"or on_key_down(), not at the top level of your file."
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _do_restart(self):
|
|
106
|
+
if self._is_restarting:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
self._is_restarting = True
|
|
110
|
+
try:
|
|
111
|
+
# Delay clock clearing until the actual restart moment
|
|
112
|
+
if self._clock and self._restart_clear_mode == "on_restart":
|
|
113
|
+
self._clock.clear()
|
|
114
|
+
# clear() may remove this scheduled callback, so make sure
|
|
115
|
+
# we do not accidentally re-enter or depend on future calls
|
|
116
|
+
|
|
117
|
+
self.reset()
|
|
118
|
+
if callable(self._start_callback):
|
|
119
|
+
self._start_callback()
|
|
120
|
+
finally:
|
|
121
|
+
self._is_restarting = False
|
|
122
|
+
|
|
123
|
+
def _end(self, state, restart_after, clear_clock):
|
|
124
|
+
"""Leave the "playing" state, optionally scheduling a restart."""
|
|
125
|
+
if self.state != "playing":
|
|
126
|
+
return
|
|
127
|
+
self._check_can_delay(restart_after)
|
|
128
|
+
self.state = state
|
|
129
|
+
|
|
130
|
+
if self._clock and clear_clock == "immediate":
|
|
131
|
+
self._clock.clear()
|
|
132
|
+
|
|
133
|
+
if restart_after is not None:
|
|
134
|
+
restart_mode = clear_clock if clear_clock != "immediate" else "never"
|
|
135
|
+
self.restart(restart_after, clear_clock=restart_mode)
|
|
136
|
+
|
|
137
|
+
def win(self, restart_after=None, clear_clock="immediate"):
|
|
138
|
+
self._end("won", restart_after, clear_clock)
|
|
139
|
+
|
|
140
|
+
def lose(self, restart_after=None, clear_clock="immediate"):
|
|
141
|
+
self._end("lost", restart_after, clear_clock)
|
|
142
|
+
|
|
143
|
+
def over(self, restart_after=None, clear_clock="immediate"):
|
|
144
|
+
self._end("over", restart_after, clear_clock)
|
|
145
|
+
|
|
146
|
+
def start(self):
|
|
147
|
+
"""Start or resume normal gameplay."""
|
|
148
|
+
self.state = "playing"
|
|
149
|
+
|
|
150
|
+
def pause(self):
|
|
151
|
+
"""Pause the game."""
|
|
152
|
+
if self.state == "playing":
|
|
153
|
+
self.state = "paused"
|
|
154
|
+
|
|
155
|
+
def resume(self):
|
|
156
|
+
"""Resume the game if paused."""
|
|
157
|
+
if self.state == "paused":
|
|
158
|
+
self.state = "playing"
|
|
159
|
+
|
|
160
|
+
def next_level(self):
|
|
161
|
+
"""Advance to the next level."""
|
|
162
|
+
self.level += 1
|
|
163
|
+
|
|
164
|
+
def add_score(self, points):
|
|
165
|
+
"""Increase the score by the given number of points."""
|
|
166
|
+
self.score += points
|
|
167
|
+
|
|
168
|
+
def tick(self, dt):
|
|
169
|
+
"""
|
|
170
|
+
Update common counters.
|
|
171
|
+
dt = time since last frame (in seconds)
|
|
172
|
+
"""
|
|
173
|
+
self.frame_count += 1
|
|
174
|
+
|
|
175
|
+
if self.state == "playing":
|
|
176
|
+
self.timer += dt
|
|
177
|
+
|
|
178
|
+
# These two methods don't change how attributes work at runtime. Their
|
|
179
|
+
# type hints tell editors and linters (e.g. Pylance) that students may
|
|
180
|
+
# add their own attributes, such as ``game.lives = 3``, without being
|
|
181
|
+
# warned that the attribute doesn't exist. Do not remove them.
|
|
182
|
+
def __getattr__(self, name: str) -> Any:
|
|
183
|
+
raise AttributeError(f"'Game' object has no attribute '{name}'")
|
|
184
|
+
|
|
185
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
186
|
+
object.__setattr__(self, name, value)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def exit():
|
|
190
|
+
"""Wait for up to a second for all sounds to play out
|
|
191
|
+
and then exit
|
|
192
|
+
"""
|
|
193
|
+
t0 = time.time()
|
|
194
|
+
while pygame.mixer.get_busy():
|
|
195
|
+
time.sleep(0.1)
|
|
196
|
+
if time.time() - t0 > 1.0:
|
|
197
|
+
break
|
|
198
|
+
sys.exit()
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def positional_parameters(handler):
|
|
202
|
+
"""Get the positional parameters of the given function."""
|
|
203
|
+
code = handler.__code__
|
|
204
|
+
return code.co_varnames[: code.co_argcount]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class DEFAULTICON:
|
|
208
|
+
"""Sentinel indicating that we want to use the default icon."""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class PGZeroGame:
|
|
212
|
+
"""The core game loop for Pygame Zero.
|
|
213
|
+
|
|
214
|
+
Dispatch events, call update functions, draw. Repeat.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def __init__(self, mod: types.ModuleType, fps: bool = False):
|
|
218
|
+
"""Construct a game loop given the pgzero module mod.
|
|
219
|
+
|
|
220
|
+
If fps is True, show a FPS count at the bottom left of the window.
|
|
221
|
+
"""
|
|
222
|
+
self.mod = mod
|
|
223
|
+
self.screen = None
|
|
224
|
+
self.width = None
|
|
225
|
+
self.height = None
|
|
226
|
+
self.title = None
|
|
227
|
+
self.icon = None
|
|
228
|
+
self.fps = fps
|
|
229
|
+
self.keyboard = pgzero.keyboard.keyboard
|
|
230
|
+
self.handlers = {}
|
|
231
|
+
|
|
232
|
+
def reinit_screen(self) -> bool:
|
|
233
|
+
"""Reinitialise the window.
|
|
234
|
+
|
|
235
|
+
Return True if the dimensions of the screen changed.
|
|
236
|
+
|
|
237
|
+
"""
|
|
238
|
+
global screen
|
|
239
|
+
changed = False
|
|
240
|
+
mod = self.mod
|
|
241
|
+
|
|
242
|
+
icon = getattr(self.mod, "ICON", DEFAULTICON)
|
|
243
|
+
if icon and icon != self.icon:
|
|
244
|
+
self.show_icon()
|
|
245
|
+
|
|
246
|
+
w = getattr(mod, "WIDTH", 800)
|
|
247
|
+
h = getattr(mod, "HEIGHT", 600)
|
|
248
|
+
if w != self.width or h != self.height:
|
|
249
|
+
self.screen = pygame.display.set_mode((w, h), DISPLAY_FLAGS, vsync=1)
|
|
250
|
+
pgzero.screen.screen_instance._set_surface(self.screen)
|
|
251
|
+
|
|
252
|
+
# Set the global screen that actors blit to
|
|
253
|
+
screen = self.screen
|
|
254
|
+
self.width = w
|
|
255
|
+
self.height = h
|
|
256
|
+
|
|
257
|
+
# Dimensions changed, request a redraw
|
|
258
|
+
changed = True
|
|
259
|
+
|
|
260
|
+
title = getattr(self.mod, "TITLE", "Pygame Zero Game")
|
|
261
|
+
if title != self.title:
|
|
262
|
+
pygame.display.set_caption(title)
|
|
263
|
+
self.title = title
|
|
264
|
+
|
|
265
|
+
return changed
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def show_default_icon():
|
|
269
|
+
"""Show a default icon loaded from Pygame Zero resources."""
|
|
270
|
+
from io import BytesIO
|
|
271
|
+
from pkgutil import get_data
|
|
272
|
+
|
|
273
|
+
buf = BytesIO(get_data(__name__, "data/icon.png"))
|
|
274
|
+
pygame.display.set_icon(pygame.image.load(buf))
|
|
275
|
+
|
|
276
|
+
def show_icon(self):
|
|
277
|
+
icon = getattr(self.mod, "ICON", DEFAULTICON)
|
|
278
|
+
if icon is DEFAULTICON:
|
|
279
|
+
self.show_default_icon()
|
|
280
|
+
else:
|
|
281
|
+
pygame.display.set_icon(pgzero.loaders.images.load(icon))
|
|
282
|
+
self.icon = icon
|
|
283
|
+
|
|
284
|
+
EVENT_HANDLERS = {
|
|
285
|
+
pygame.MOUSEBUTTONDOWN: "on_mouse_down",
|
|
286
|
+
pygame.MOUSEBUTTONUP: "on_mouse_up",
|
|
287
|
+
pygame.MOUSEMOTION: "on_mouse_move",
|
|
288
|
+
pygame.KEYDOWN: "on_key_down",
|
|
289
|
+
pygame.KEYUP: "on_key_up",
|
|
290
|
+
constants.MUSIC_END: "on_music_end",
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
def map_buttons(val):
|
|
294
|
+
return {c for c, pressed in zip(constants.mouse, val) if pressed}
|
|
295
|
+
|
|
296
|
+
EVENT_PARAM_MAPPERS = {"buttons": map_buttons, "button": constants.mouse, "key": constants.keys}
|
|
297
|
+
|
|
298
|
+
def load_handlers(self):
|
|
299
|
+
from .spellcheck import spellcheck
|
|
300
|
+
|
|
301
|
+
spellcheck(vars(self.mod))
|
|
302
|
+
self.handlers = {}
|
|
303
|
+
for type, name in self.EVENT_HANDLERS.items():
|
|
304
|
+
handler = getattr(self.mod, name, None)
|
|
305
|
+
if callable(handler):
|
|
306
|
+
self.handlers[type] = self.prepare_handler(handler)
|
|
307
|
+
|
|
308
|
+
def prepare_handler(self, handler):
|
|
309
|
+
"""Adapt a pgzero game's raw handler function to take a Pygame Event.
|
|
310
|
+
|
|
311
|
+
Returns a one-argument function of the form ``handler(event)``.
|
|
312
|
+
This will ensure that the correct arguments are passed to the raw
|
|
313
|
+
handler based on its argument spec.
|
|
314
|
+
|
|
315
|
+
The wrapped handler will also map certain parameter values using
|
|
316
|
+
callables from EVENT_PARAM_MAPPERS; this ensures that the value of
|
|
317
|
+
'button' inside the handler is a real instance of constants.mouse,
|
|
318
|
+
which means (among other things) that it will print as a symbolic value
|
|
319
|
+
rather than a naive integer.
|
|
320
|
+
|
|
321
|
+
"""
|
|
322
|
+
code = handler.__code__
|
|
323
|
+
param_names = code.co_varnames[: code.co_argcount]
|
|
324
|
+
|
|
325
|
+
def make_getter(mapper, getter):
|
|
326
|
+
if mapper:
|
|
327
|
+
return lambda event: mapper(getter(event))
|
|
328
|
+
return getter
|
|
329
|
+
|
|
330
|
+
param_handlers = []
|
|
331
|
+
for name in param_names:
|
|
332
|
+
getter = operator.attrgetter(name)
|
|
333
|
+
mapper = self.EVENT_PARAM_MAPPERS.get(name)
|
|
334
|
+
param_handlers.append((name, make_getter(mapper, getter)))
|
|
335
|
+
|
|
336
|
+
def prep_args(event):
|
|
337
|
+
return {name: get(event) for name, get in param_handlers}
|
|
338
|
+
|
|
339
|
+
def new_handler(event):
|
|
340
|
+
try:
|
|
341
|
+
prepped = prep_args(event)
|
|
342
|
+
except ValueError:
|
|
343
|
+
# If we couldn't construct the keys/mouse objects representing
|
|
344
|
+
# the button that was pressed, then skip the event handler.
|
|
345
|
+
#
|
|
346
|
+
# This happens because Pygame can generate key codes that it
|
|
347
|
+
# does not have constants for.
|
|
348
|
+
return
|
|
349
|
+
else:
|
|
350
|
+
return handler(**prepped)
|
|
351
|
+
|
|
352
|
+
return new_handler
|
|
353
|
+
|
|
354
|
+
def get_update_func(self):
|
|
355
|
+
"""Get a one-argument update function.
|
|
356
|
+
|
|
357
|
+
If the module defines a function matching ::
|
|
358
|
+
|
|
359
|
+
update(dt)
|
|
360
|
+
|
|
361
|
+
or ::
|
|
362
|
+
|
|
363
|
+
update()
|
|
364
|
+
|
|
365
|
+
then this will be called. Otherwise return a no-op function.
|
|
366
|
+
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
update = self.mod.update
|
|
370
|
+
except AttributeError:
|
|
371
|
+
return None
|
|
372
|
+
else:
|
|
373
|
+
if update.__code__.co_argcount == 0:
|
|
374
|
+
return lambda dt: update()
|
|
375
|
+
return update
|
|
376
|
+
|
|
377
|
+
def get_draw_func(self):
|
|
378
|
+
"""Get a draw function.
|
|
379
|
+
|
|
380
|
+
If no draw function is define, raise an exception.
|
|
381
|
+
|
|
382
|
+
"""
|
|
383
|
+
try:
|
|
384
|
+
draw = self.mod.draw
|
|
385
|
+
except AttributeError:
|
|
386
|
+
return lambda: None
|
|
387
|
+
else:
|
|
388
|
+
if draw.__code__.co_argcount != 0:
|
|
389
|
+
raise TypeError("draw() must not take any arguments.")
|
|
390
|
+
return draw
|
|
391
|
+
|
|
392
|
+
def get_start_func(self):
|
|
393
|
+
"""Get the start() function from the user module if it exists."""
|
|
394
|
+
start = getattr(self.mod, "start", None)
|
|
395
|
+
if start is not None and not callable(start):
|
|
396
|
+
raise TypeError(f"{start!r} is not callable")
|
|
397
|
+
return start
|
|
398
|
+
|
|
399
|
+
def run(self):
|
|
400
|
+
"""Invoke the main loop, and then clean up."""
|
|
401
|
+
try:
|
|
402
|
+
self.mainloop()
|
|
403
|
+
finally:
|
|
404
|
+
pygame.display.quit()
|
|
405
|
+
pygame.mixer.quit()
|
|
406
|
+
|
|
407
|
+
def inject_global_handlers(self):
|
|
408
|
+
"""Inject handlers provide by the Pygame Zero system.
|
|
409
|
+
|
|
410
|
+
Some of these wrap user handlers so must be injected later.
|
|
411
|
+
"""
|
|
412
|
+
self.handlers[pygame.QUIT] = lambda e: sys.exit(0)
|
|
413
|
+
self.handlers[pygame.VIDEOEXPOSE] = lambda e: None
|
|
414
|
+
|
|
415
|
+
user_key_down = self.handlers.get(pygame.KEYDOWN)
|
|
416
|
+
user_key_up = self.handlers.get(pygame.KEYUP)
|
|
417
|
+
|
|
418
|
+
def key_down(event):
|
|
419
|
+
if event.key == pygame.K_q and event.mod & (pygame.KMOD_CTRL | pygame.KMOD_META):
|
|
420
|
+
sys.exit(0)
|
|
421
|
+
self.keyboard._press(event.key)
|
|
422
|
+
if user_key_down:
|
|
423
|
+
return user_key_down(event)
|
|
424
|
+
|
|
425
|
+
def key_up(event):
|
|
426
|
+
self.keyboard._release(event.key)
|
|
427
|
+
if user_key_up:
|
|
428
|
+
return user_key_up(event)
|
|
429
|
+
|
|
430
|
+
self.handlers[pygame.KEYDOWN] = key_down
|
|
431
|
+
self.handlers[pygame.KEYUP] = key_up
|
|
432
|
+
|
|
433
|
+
def tick_game_state(self, dt):
|
|
434
|
+
"""Tick the global game object if it exists."""
|
|
435
|
+
game_obj = getattr(self.mod, "game", None)
|
|
436
|
+
|
|
437
|
+
if game_obj is None:
|
|
438
|
+
game_obj = getattr(builtins, "game", None)
|
|
439
|
+
|
|
440
|
+
if game_obj is None:
|
|
441
|
+
return False
|
|
442
|
+
|
|
443
|
+
tick = getattr(game_obj, "tick", None)
|
|
444
|
+
if not callable(tick):
|
|
445
|
+
return False
|
|
446
|
+
|
|
447
|
+
tick(dt)
|
|
448
|
+
return True
|
|
449
|
+
|
|
450
|
+
def handle_events(self, dt, update) -> bool:
|
|
451
|
+
"""Handle all events for the current frame.
|
|
452
|
+
|
|
453
|
+
Return True if an event was handled.
|
|
454
|
+
"""
|
|
455
|
+
updated = False
|
|
456
|
+
|
|
457
|
+
for event in pygame.event.get():
|
|
458
|
+
if event.type == constants.MUSIC_END:
|
|
459
|
+
# internal music bookkeeping
|
|
460
|
+
from . import music
|
|
461
|
+
|
|
462
|
+
music._on_music_end()
|
|
463
|
+
|
|
464
|
+
handler = self.handlers.get(event.type)
|
|
465
|
+
if handler:
|
|
466
|
+
handler(event)
|
|
467
|
+
updated = True
|
|
468
|
+
|
|
469
|
+
clock = pgzero.clock.clock
|
|
470
|
+
clock.tick(dt)
|
|
471
|
+
updated |= clock.fired
|
|
472
|
+
|
|
473
|
+
updated |= self.tick_game_state(dt)
|
|
474
|
+
|
|
475
|
+
if update:
|
|
476
|
+
update(dt)
|
|
477
|
+
updated = True
|
|
478
|
+
|
|
479
|
+
updated |= self.reinit_screen()
|
|
480
|
+
return updated
|
|
481
|
+
|
|
482
|
+
def mainloop(self):
|
|
483
|
+
"""Run the main loop of Pygame Zero."""
|
|
484
|
+
self.reinit_screen()
|
|
485
|
+
|
|
486
|
+
update = self.get_update_func()
|
|
487
|
+
draw = self.get_draw_func()
|
|
488
|
+
start = self.get_start_func()
|
|
489
|
+
|
|
490
|
+
game_obj = getattr(self.mod, "game", None)
|
|
491
|
+
if game_obj is None:
|
|
492
|
+
game_obj = getattr(builtins, "game", None)
|
|
493
|
+
|
|
494
|
+
if game_obj is not None and hasattr(game_obj, "_bind_engine"):
|
|
495
|
+
game_obj._bind_engine(start_callback=start, clock=pgzero.clock.clock)
|
|
496
|
+
|
|
497
|
+
self.load_handlers()
|
|
498
|
+
self.inject_global_handlers()
|
|
499
|
+
|
|
500
|
+
if start:
|
|
501
|
+
start()
|
|
502
|
+
|
|
503
|
+
logic_timer = Timer("logic", print=self.fps)
|
|
504
|
+
draw_timer = Timer("draw", print=self.fps)
|
|
505
|
+
for i, dt in enumerate(frames(60)):
|
|
506
|
+
with logic_timer:
|
|
507
|
+
updated = self.handle_events(dt, update)
|
|
508
|
+
|
|
509
|
+
if updated:
|
|
510
|
+
with draw_timer:
|
|
511
|
+
draw()
|
|
512
|
+
|
|
513
|
+
if self.fps and i and i % 60 == 0:
|
|
514
|
+
ftime_ms = draw_timer.get_mean() + logic_timer.get_mean()
|
|
515
|
+
fps = 1000 / ftime_ms
|
|
516
|
+
|
|
517
|
+
print(f"fps: {fps:0.1f} time per frame: {ftime_ms:0.1f}ms")
|
|
518
|
+
pygame.display.flip()
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def frames(fps=60):
|
|
522
|
+
"""Iterate over frames at the given fps, yielding time delta (in s)."""
|
|
523
|
+
tgt = 1 / fps # target frame time
|
|
524
|
+
|
|
525
|
+
t = perf_counter()
|
|
526
|
+
dt = tgt
|
|
527
|
+
|
|
528
|
+
while True:
|
|
529
|
+
yield dt
|
|
530
|
+
nextt = perf_counter()
|
|
531
|
+
dt = nextt - t
|
|
532
|
+
if dt < tgt:
|
|
533
|
+
sleep(tgt - dt)
|
|
534
|
+
nextt = perf_counter()
|
|
535
|
+
dt = nextt - t
|
|
536
|
+
t = nextt
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
class Timer:
|
|
540
|
+
"""Context manager to time the game loop."""
|
|
541
|
+
|
|
542
|
+
__slots__ = (
|
|
543
|
+
"name",
|
|
544
|
+
"total",
|
|
545
|
+
"count",
|
|
546
|
+
"worst",
|
|
547
|
+
"start",
|
|
548
|
+
"print",
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
def __init__(self, name, print=False):
|
|
552
|
+
self.name = name
|
|
553
|
+
self.total = 0
|
|
554
|
+
self.count = 0
|
|
555
|
+
self.worst = 0
|
|
556
|
+
self.print = print
|
|
557
|
+
|
|
558
|
+
def __enter__(self):
|
|
559
|
+
self.start = perf_counter()
|
|
560
|
+
|
|
561
|
+
def __exit__(self, *_):
|
|
562
|
+
t = (perf_counter() - self.start) * 1e3
|
|
563
|
+
self.count += 1
|
|
564
|
+
self.total += t
|
|
565
|
+
if t > self.worst:
|
|
566
|
+
self.worst = t
|
|
567
|
+
|
|
568
|
+
def get_mean(self) -> float:
|
|
569
|
+
mean = self.total / self.count
|
|
570
|
+
if self.print:
|
|
571
|
+
print(f"{self.name} mean: {mean:0.1f}ms " f"worst: {self.worst:0.1f}ms")
|
|
572
|
+
self.worst = self.total = self.count = 0
|
|
573
|
+
return mean
|
pgzero/keyboard.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from warnings import warn
|
|
3
|
+
|
|
4
|
+
from .constants import keys
|
|
5
|
+
|
|
6
|
+
DEPRECATED_KEY_RE = re.compile(r"[A-Z]")
|
|
7
|
+
PREFIX_RE = re.compile(r"^K_(?!\d$)")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Keyboard:
|
|
11
|
+
"""The current state of the keyboard.
|
|
12
|
+
|
|
13
|
+
Each attribute represents a key. For example, ::
|
|
14
|
+
|
|
15
|
+
keyboard.a
|
|
16
|
+
|
|
17
|
+
is True if the 'A' key is depressed, and False otherwise.
|
|
18
|
+
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# The current key state. This may as well be a class attribute - there's
|
|
22
|
+
# only one keyboard.
|
|
23
|
+
_pressed = set()
|
|
24
|
+
|
|
25
|
+
def __getattr__(self, kname):
|
|
26
|
+
# return is a reserved word, so alias enter to return
|
|
27
|
+
if kname == "enter":
|
|
28
|
+
kname = "return"
|
|
29
|
+
elif DEPRECATED_KEY_RE.match(kname):
|
|
30
|
+
# warn(
|
|
31
|
+
# "Uppercase keyboard attributes (eg. keyboard.%s) are "
|
|
32
|
+
# "deprecated." % kname,
|
|
33
|
+
# DeprecationWarning,
|
|
34
|
+
# 2
|
|
35
|
+
# )
|
|
36
|
+
kname = PREFIX_RE.sub("", kname)
|
|
37
|
+
try:
|
|
38
|
+
key = keys[kname.upper()]
|
|
39
|
+
except AttributeError:
|
|
40
|
+
raise AttributeError('The key "%s" does not exist' % key)
|
|
41
|
+
return key.value in self._pressed
|
|
42
|
+
|
|
43
|
+
def _press(self, key):
|
|
44
|
+
"""Called by Game to mark the key as pressed."""
|
|
45
|
+
self._pressed.add(key)
|
|
46
|
+
|
|
47
|
+
def _release(self, key):
|
|
48
|
+
"""Called by Game to mark the key as released."""
|
|
49
|
+
self._pressed.discard(key)
|
|
50
|
+
|
|
51
|
+
def __getitem__(self, k):
|
|
52
|
+
if isinstance(k, keys):
|
|
53
|
+
return k.value in self._pressed
|
|
54
|
+
else:
|
|
55
|
+
# warn(
|
|
56
|
+
# "String lookup in keyboard (eg. keyboard[%r]) is "
|
|
57
|
+
# "deprecated." % k,
|
|
58
|
+
# DeprecationWarning,
|
|
59
|
+
# 2,
|
|
60
|
+
# )
|
|
61
|
+
return getattr(self, k)
|
|
62
|
+
|
|
63
|
+
def __repr__(self):
|
|
64
|
+
return "<Keyboard pressed={}>".format(self._pressed)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
keyboard = Keyboard()
|