-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathbook_chapter2.txt
429 lines (347 loc) · 13.9 KB
/
book_chapter2.txt
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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
As promised, let's now take a look at the problem of how adding of features
causes our event handling function to get hairier than the warmest yeti.
[TODO: maybe a note about how if you're really looking for quick development
and don't want see the steps or the rationale, you can skip to the end of
this chapter for a complete event manager module]
----
def handle_events(clock):
for event in pygame.event.get():
if event.type == c.QUIT:
return False
elif event.type == c.MOUSEBUTTONDOWN:
for sprite in sprites:
if isinstance(sprite, Monkey):
sprite.attempt_punch(event.pos)
clock.tick(60) # aim for 60 frames per second
for sprite in sprites:
sprite.update()
return True
----
We can see that this function grows in complexity as 3 other things increase:
event sources (places that events come from)
event types
event listeners (places where events need to be sent)
----
def handle_events(clock):
for event in pygame.event.get():
if event.type == c.QUIT:
return False
elif event.type == c.MOUSEBUTTONDOWN:
for sprite in sprites:
if isinstance(sprite, Monkey):
sprite.attempt_punch(event.pos)
elif event.type == event_type_B:
for sprite in sprites:
if isinstance(sprite, Trap):
...
elif event.type == event_type_C:
...
elif event.type == event_type_D:
...
clock.tick(60) # aim for 60 frames per second
for sprite in sprites:
sprite.update()
for event in some_other_events():
...
specialEvent = from_somewhere()
for sprite in sprites:
sprite.do_special()
----
Remember our two tools when faced with complexity: organization and abstraction.
Let's use some abstraction. We can move from handling each sink, source,
or event type in it's own specific block of code to a single block of code that
treats them generally.
Start with the event sources. We can see 4 different sources above, two
we've seen before, pygame.event.get(), and clock.tick()/sprite.update(), as
well as two new ones that exist just to provide an example of how the event
sources might grow, some_other_events() and from_somewhere()/do_special().
In general, all 4 do the same thing: generate events that must be sent
to the listeners. So let's group them into a function.
----
def generate_events(clock):
for event in pygame.event.get():
yield event
clock.tick(60) # aim for 60 frames per second
yield 'ClockTick'
for event in some_other_events():
yield event
specialEvent = from_somewhere()
yield 'SpecialEvent'
def handle_events(clock):
for event in generate_events(clock):
if event == 'ClockTick':
for sprite in sprites:
sprite.update()
elif event == 'SpecialEvent':
for sprite in sprites:
sprite.do_special()
... # handle those events that came from some_other_events()
elif event.type == c.QUIT:
return False
elif event.type == c.MOUSEBUTTONDOWN:
for sprite in sprites:
if isinstance(sprite, Monkey):
sprite.attempt_punch(event.pos)
elif event.type == event_type_B:
for sprite in sprites:
if isinstance(sprite, Trap):
...
elif event.type == event_type_C:
...
elif event.type == event_type_D:
...
return True
----
This generate_events() function is a little ugly, because it returns two types
of object and relies on the caller to know how to handle them. So let's
address the complexity caused by multiplying event types next. Here's how
to treate each event type in a general way:
----
class Monkey(pygame.sprite.Sprite):
...
def on_event(self, event):
if event == 'ClockTick':
self.update()
elif event == 'SpecialEvent':
self.do_special()
... # handle those events that came from some_other_events()
elif event.type == c.MOUSEBUTTONDOWN:
self.attempt_punch(event.pos)
# notice that Monkey doesn't do anything on event_type_B
elif event.type == event_type_C:
...
elif event.type == event_type_D:
...
class Trap(pygame.sprite.Sprite):
...
def on_event(self, event):
if event == 'ClockTick':
self.update()
elif event == 'SpecialEvent':
self.do_special()
... # handle those events that came from some_other_events()
# notice that Trap doesn't do anything on MOUSEBUTTONDOWN
elif event.type == event_type_B:
self.add_some_honey()
elif event.type == event_type_C:
...
elif event.type == event_type_D:
...
def handle_events(clock):
for event in generate_events(clock):
if hasattr(event, 'type') and event.type == c.QUIT:
return False
for sprite in sprites:
sprite.on_event(event)
return True
----
Now we have *more* lines of code. Have we gone backwards? Not really.
Remember, we are trying to come up with a good way to deal with increasing
complexity. If we add 10 more events that only affect Monkeys, the only
place we need to add more code will be in the Monkey class. The Trap class
and the handle_events() function will be unaffected.
So the increase of event types destined for specific classes has been
addressed, but what about event types that apply to many classes. An example
of such an event is what is now the 'ClockTick' string. Every sprite will
conceivably need to call update() when it receives a 'ClockTick'.
We can use some of the language features that make Python so joyous to use to
solve this issue:
[TODO: talk about the features we're about to use in a general sense, and address possible resistance]
----
class EventHandlingSprite(pygame.sprite.Sprite):
def on_ClockTick(self):
self.update()
def on_Special(self):
...
class Monkey(EventHandlingSprite):
...
def on_PygameEvent(self, event):
if event.type == c.MOUSEBUTTONDOWN:
self.attempt_punch(event.pos)
elif event.type == event_type_C:
...
elif event.type == event_type_D:
...
class Trap(EventHandlingSprite):
...
def on_PygameEvent(self, event):
if event.type == event_type_B:
self.add_some_honey()
elif event.type == event_type_C:
...
elif event.type == event_type_D:
...
def generate_events(clock):
for event in pygame.event.get():
yield ('PygameEvent', event)
clock.tick(60) # aim for 60 frames per second
yield ('ClockTick', )
for event in some_other_events():
yield (event.__class__.__name__, event)
specialEvent = from_somewhere()
yield ('SpecialEvent', )
def handle_events(clock):
for eventTuple in generate_events(clock):
if eventTuple[0] == 'PygameEvent' and eventTuple[1].type == c.QUIT:
return False
for sprite in sprites:
methodName = 'on_' + eventTuple[0]
if hasattr(sprite, methodName):
method = getattr(sprite, methodName)
method(*eventTuple[1:])
return True
----
This was a big change, so let's examine it step by step. First, look at the
generate_events() function. Now, instead of yielding two different types,
either a string or some event instance, it consistently yields tuples.
Every tuple has a string as its first argument. The string is a somewhat
descriptive name for the event.
This name gets consumed by the handle_events() function, which prefixes it
with "on_". For example, it takes "ClockTick" and creates the string
"on_ClockTick". It then looks for a method matching that name in every sprite.
If it finds one, it calls the method, using the remaining items inside the
tuple as arguments. For example, when it calls on_PygameEvent(), it uses
the event as an argument.
We see that on_PygameEvent() has been implemented in both the Monkey and
Trap classes.
Since on_ClockTick() and on_Special() will do the same thing to
either a Monkey or a Trap, a new superclass is created to avoid duplicating
those methods in both the Monkey and Trap classes.
One change remains, the treatment of events that come from some_other_events().
Let's assume that such events are proper Python instances and we can access
their magic __class__ attribute (we can't do this with Pygame events). When
this is the case, we don't need to come up with our own descriptive string,
we can just use __class__.__name__.
So if some_other_events() is extended to return events of a new class, "Foo",
we don't need to change generate_events() or handle_events(), we just need to
add a on_Foo() method to one of our sprite classes. The dark forces of
Complexity are truly cowering now.
[TODO: Aside - discuss the possible infinite loop if some_other_events() was
a generator that could get new events as listeners processed events]
Next, let's address the growth of event listeners. We've already taken care
of the addition of new sprites with our earlier changes, but what if we want
other types of objects to listen to events? For example, a log file object.
Simply add a data structure that collects all the listeners.
While making this change, let's also notice that our function, handle_events()
is no longer doing any *handling* of the events. So it will be renamed
dispatch_events() to more accurately reflect what it does.
----
listeners = []
def init():
# Necessary Pygame set-up...
pygame.init()
clock = pygame.time.Clock()
displayImg = pygame.display.set_mode(screenDimensions.size)
monkey = Monkey()
sprites.add(monkey)
listeners += sprites
return (clock, displayImg)
def dispatch_events(clock):
for eventTuple in generate_events(clock):
for listener in listeners:
methodName = 'on_' + eventTuple[0]
if hasattr(listener, methodName)
method = getattr(listener, methodName)
methodName(*eventTuple[1:])
----
Now hopefully your organizational instincts are telling you that with our
handful of functions and a module-level data structure that are all
inter-related, it is a good time to encapsulate them into their own module.
[TODO: talk about Mediator pattern here?]
Your instincts have perhaps also been keeping you annoyed by the passing of the
Pygame clock object two levels deep, from dispatch_events() to
generate_events(). The next change will address this as well.
----
#! /usr/bin/python
'''Event module'''
listeners = []
sources = []
def addListener(listener):
listeners.append(listener)
def addSource(sourceFn, eventTypeName=None):
'''Add an "event source".
sourceFn must be callable, and must return a list when called.
Note: not an iterable, but an actual, fixed-length list.
It is recommended that this function be used by foreign modules. The foreign
module should provide a sourceFn that returns all the local events that
have happened since it was last called
eventTypeName is optional. If included, it should be a string that will
be added to "on_" to create method names to be searched for in the
listeners
'''
sources.append((sourceFn, eventTypeName))
def generate_events():
for sourceFn, eventTypeName in sources:
for event in sourceFn():
if eventTypeName:
typeName = eventTypeName
else:
typeName = event.__class__.__name__
yield (typeName, event)
def dispatch_events():
for typeName, event in generate_events():
for listener in listeners:
methodName = 'on_' + typeName
if hasattr(listener, methodName)
method = getattr(listener, methodName)
methodName(event)
----
Now to make this nice for our rapid develompent goal.
Some modules don't want to be so sophisticated with their own sourceFn and
diamond earrings and tophats. Let's provide a simple way for them to just
post events into the queue, a single function called post() that doesn't
need to take any arguments fancier than a simple string. (Though it is
flexible enough to handle more arguments)
----
localEvents = []
def getLocalEvents():
global localEvents
localEventsCopy = localEvents[:]
localEvents = []
return localEventsCopy
addSource(getLocalEvents)
def post(evName, *extraArgs, **kwargs):
class AnonymousEvent(object): pass
AnonymousEvent.__name__ = evName
event = AnonymousEvent()
event.name = evName
event.args = extraArgs
event.kwargs = kwargs
localEvents.append(event)
----
Now let's integrate our new events.py module into our main loop.
----
import events
sprites = pygame.sprite.Group()
keepGoing = True
def init():
# Necessary Pygame set-up...
pygame.init()
clock = pygame.time.Clock()
displayImg = pygame.display.set_mode(screenDimensions.size)
monkey = Monkey()
sprites.add(monkey)
return (clock, displayImg)
def draw_to_display(displayImg):
displayImg.fill(black)
for sprite in sprites:
displayImg.blit(sprite.image, sprite.rect)
pygame.display.flip()
class QuitListener(object):
def __init__(self):
self.quit = False
def on_PygameEvent(self, event):
if event.type == c.QUIT:
self.quit = True
def main():
clock, displayImg = init()
events.addSource(pygame.event.get, 'PygameEvent')
quitter = QuitListener()
events.addListener(quitter)
while not quitter.quit:
events.dispatch_events()
draw_to_display(displayImg)
clock.tick(60) # aim for 60 frames per second
events.post('ClockTick')
----
[TODO: what about the issue of getting things in order when there's a mix of eventSourceFunctions and just calls to post()? Maybe we don't have to worry about it - let them shoot themselves in the foot]