forked from sjbrown/writing_games_tutorial
-
Notifications
You must be signed in to change notification settings - Fork 0
/
book_chapter1.txt
382 lines (313 loc) · 14.7 KB
/
book_chapter1.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
Let's start with a very simple example of a game. This will be a game in which
a monkey's face travels back and forth across the screen, and the player must
try to "punch" the monkey by clicking on it. The gameplay will be familiar to
anyone who has gone through the "Chimp Line by Line" [TODO: link] tutorial or
those who have endured annoying banner ads in the early 2000s.
----
import time
import pygame
import pygame.constants as c
score = 0
screenDimensions = pygame.Rect((0,0,400,60))
black = (0,0,0)
white = (255,255,255)
blue = (0,0,255)
red = (255,0,0)
class Monkey(pygame.sprite.Sprite):
def __init__(self):
self.stunTimeout = None
self.velocity = 2
super(Monkey, self).__init__()
self.image = pygame.Surface((60,60))
self.rect = self.image.get_rect()
self.render(blue)
def render(self, color):
'''draw onto self.image the face of a monkey in the specified color'''
self.image.fill(color)
pygame.draw.circle(self.image, white, (10,10), 10, 2)
pygame.draw.circle(self.image, white, (50,10), 10, 2)
pygame.draw.circle(self.image, white, (30,60), 20, 2)
def attempt_punch(self, pos):
'''If the given position (pos) is inside the monkey's rect, the monkey
has been "punched". A successful punch will stun the monkey and
increment the global score.
The monkey cannot be punched if he is already stunned
'''
if self.stunTimeout:
return # already stunned
if self.rect.collidepoint(pos):
# Argh! The punch intersected with my face!
self.stunTimeout = time.time() + 2 # 2 seconds from now
global score
score += 1
self.render(red)
def update(self):
if self.stunTimeout:
# If stunned, the monkey doesn't move
if time.time() > self.stunTimeout:
self.stunTimeout = None
self.render(blue)
else:
# Move the monkey
self.rect.x += self.velocity
# Don't let the monkey run past the edge of the viewable area
if self.rect.right > screenDimensions.right:
self.velocity = -2
elif self.rect.left < screenDimensions.left:
self.velocity = 2
def main():
# Necessary Pygame set-up...
pygame.init()
clock = pygame.time.Clock()
displayImg = pygame.display.set_mode(screenDimensions.size)
monkey = Monkey()
while True:
clock.tick(60) # aim for 60 frames per second
for event in pygame.event.get():
if event.type == c.QUIT:
return
elif event.type == c.MOUSEBUTTONDOWN:
monkey.attempt_punch(event.pos)
monkey.update()
displayImg.fill(black)
displayImg.blit(monkey.image, monkey.rect)
pygame.display.flip()
if __name__ == '__main__':
main()
print 'Your score was', score
----
So with that we have a (very simple, but complete) game. It may not be the most
fun game ever written, but that can be fixed by slick box art and a major motion
picture tie-in. Let's leave those concerns for the marketing department and
instead look at the technical details.
What we have above is a minimal game. As we add features to it, the code will
grow in complexity. As humans, we are bad at holding and manipulating complex
systems in our brains.
Consider what would happen if instead of just punching one monkey, we wanted to set
traps for 3 monkeys. A click of the mouse would either drop down a trap at the
clicked location or reset a sprung trap if one was already there. What might
our main() function look like?
----
def main():
# Necessary Pygame set-up...
pygame.init()
clock = pygame.time.Clock()
displayImg = pygame.display.set_mode(screenDimensions.size)
monkeys = [Monkey(), Monkey(), Monkey()]
traps = [Trap(), Trap(), Trap()]
trapCycle = itertools.cycle(traps)
while True:
clock.tick(60) # aim for 60 FPS
for event in pygame.event.get():
if event.type == c.QUIT:
return
elif event.type == c.MOUSEBUTTONDOWN:
wasTrapClick = False
for trap in traps:
if trap.rect.collidepoint(event.pos):
trap.reset()
wasTrapClick = True
break
if not wasTrapClick:
# if the user didn't click on a trap, then they
# intended to place the next one here.
trap = trapCycle.next()
trap.place_at(event.pos)
for monkey in monkeys:
monkey.update(traps)
displayImg.fill(black)
for sprite in monkeys + traps:
displayImg.blit(sprite.image, sprite.rect)
pygame.display.flip()
----
So what happened? Significantly, the block of code that starts with
"for event in pygame.event.get():" has grown. I'm going to call this the event
handling block. Now it's about 10 lines longer. It contains one new loop,
and two new branches (if statements). Imagine what will happen to
the event handling block as each new feature is added. If your imagination
is summoning images of a single skyscraping ladder of an if / elif, ridden with
deep sub-blocks of loops and branches, countless and tentacle-like, then you
are two things: accurate, and likely on the same medication as myself.
Not only will complex code be difficult to hold in your brain, it also gets
in the way of a critical goal - Rapid Development. Developing software always
involves going back to code you've written in the past to make changes. If the
code is complex, you are going to pay greater time costs for both searching
for the code to change, and for the change itself because it will need to be
made in more places.
Because we humans have trouble with complex systems, we have developed
the techniques of organization and abstraction.
We organize so that we only need to deal with one thing at a time, and
we abstract so that we can manipulate a simple system that is "similar
enough" to the complex system.
How can we organize and/or abstract this code to address the problem of
growing complexity as we add more game features? (And while we're solving
that, can we also do ourselves some favours along the way to make it faster
to develop our game?)
Luckily for us humans, our brains are *built* for this task.
They are Automatic Abstraction Apparati. We make abstractions every time
we think, and especially when we talk.
So one exercise to do is to simply talk about code. If somebody asked,
"What does this main() function do?", a reply might go something like
"Well, it does some initialization of the important objects, then it starts
this infinite 'while True:' loop, see? Inside the loop it does this
clock.tick() thing, I'm not really sure what that's for. Anyway, then it goes
through all the 'pygame' events and handles them. After all that, it calls
monkey.update() (we've got to update the monkey every frame so that it moves),
and then it draws everything to the screen."
[ASIDE]
[
Ok, did you catch that? Here you are talking about this great monkey-punching
game you wrote, and you don't even know what clock.tick() does?
clock.tick() is used to get a target *frame rate*. We want the game to look
"smooth". Animation works because if we see a series of images in quick
succession, we are tricked into thinking we are seeing a moving thing.
Try the example code with 5 as the argument to clock.tick(). The monkey no
longer looks like it is smoothly moving, instead it is jerking. That's not
acceptable for a game, nobody wants to play with a jerking monkey.
24 frames per second (FPS) is the rate used in feature films, and is generally
accepted as a minimum for video games.
By calling clock.tick(60), we are asking the operating system to *block* this
process for 1/60th of a second. When a process is blocked, it cannot
execute any further code, it just sits on a shelf, gathering nano-dust.
When the requested duration is up, the operating system puts the process
back into the mix, and its code can start executing again.
[[TODO: make sure this is technically accurate. tick() may actually do better
wall-clock FPS simulation by not blocking for 1/60th of a second, but rather
1/60th minus the time it took since the last call to tick()]]
So why stop at 60? Why not go up to 120? 240? 2000? There are a couple
reasons. One is that the game gets too fast at those rates (try it and see).
Another is that it heats up the CPU, which can be uncomfortable when using
a laptop.
Now that you know that clock.tick() is to block the process for 1/60th of a
second, think about what monkey.update() does. It's basically just a call to
inform the monkey object that 1/60th of a second has passed.
]
So to summarize what we said, the code is at the base level, initialization
then an infinite loop. Inside that loop there is an event handling block
(here, we include the call to monkey.update() as part of the event handling
block), and then a section where images are drawn to the screen. Use that
summary to organize the code like so:
----
def init():
# Necessary Pygame set-up...
pygame.init()
clock = pygame.time.Clock()
displayImg = pygame.display.set_mode(screenDimensions.size)
monkey = Monkey()
return (clock, displayImg, monkey)
def handle_events(clock, monkey):
for event in pygame.event.get():
if event.type == c.QUIT:
return False
elif event.type == c.MOUSEBUTTONDOWN:
monkey.attempt_punch(event.pos)
clock.tick(60) # aim for 60 frames per second
monkey.update()
return True
def draw_to_display(displayImg, monkey):
displayImg.fill(black)
displayImg.blit(monkey.image, monkey.rect)
pygame.display.flip()
def main():
clock, displayImg, monkey = init()
keepGoing = True
while keepGoing:
keepGoing = handle_events(clock, monkey)
draw_to_display(displayImg, monkey)
----
Look at that code. It's ever so organized. Therefore, problem solved.
We are now great coders who deserve a cookie and a pat on the back.
Don't choke on that cookie. First, ask yourself whether this change has
actually done anything worthwhile.
The code has definitely been broken into chunks that have a semantic distinction
for the reader. The functions are named descriptively, and the lines of code
in each function are fewer. These are all good things.
What if we add the 3 traps, 3 monkeys feature discussed above? We will have to
change the code as before *plus* we'll have to change all the argument passing.
Using function arguments as a river to move your little boats downstream should
raise a red flag.
Let's start abstracting. See what the code looks like if we add a
module-level variable to contain any and all sprites.
[[TODO: justify module-level variables to the no-globals-kneejerk]]
----
sprites = pygame.sprite.Group()
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 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
def draw_to_display(displayImg):
displayImg.fill(black)
for sprite in sprites:
displayImg.blit(sprite.image, sprite.rect)
pygame.display.flip()
def main():
clock, displayImg = init()
keepGoing = True
while keepGoing:
keepGoing = handle_events(clock)
draw_to_display(displayImg)
----
This change adds a few lines of code, but it got the monkey off main()'s back.
If we add 3 monkeys and 3 traps, no changes will be needed in main() (or in
draw_to_display(), for that matter).
What we have just done is partially implemented the design pattern,
"Model View Controller" (MVC).
[ASIDE]
[
Design Patterns are a communication tool; they do not dictate design, they
inform the reading of the code. This book makes use of the design patterns
"Model View Controller" (MVC), "Mediator", and "Lazy Proxy". Time won't be
spent describing these patterns in detail, so if they sound foreign to you,
I recommend checking out the book "Design Patterns" by Gamma et al. or just
surfing the web for tutorials.
]
In our example, the Model is the "sprites" object, it holds the state of our
game, any questions about the authoritative facts of the game will be directed
there. The View the draw_to_display() function, it shows a representation
of the Model on a Pygame window.
We still have the issue of the event handling code growing wildly as more
features are added, but at least we've isolated that problem to one place.
We'll tackle the problem in depth in Chapter 2.
If we want to be complete and formal about this MVC pattern, we may want to
also identify Controller components. Identifying a Controller component is
a bit trickier. One might be tempted to say that the mouse and keyboard are
the Controllers. These are indeed Controllers in one sense, but we don't
have objects in our code representing each. (and one shouldn't add classes to
the codebase just so we can have a more literal match to the Design Pattern)
Instead, these literal devices are represented by the Pygame event queue.
Also, the Pygame Clock object also serves as a Controller.
[[TODO: do I make the claim here that all event handling is a Controller? Come
back to this]]
[ASIDE]
[
Rationale
Readers with some experience writing games may be balking at this point,
thinking that a MVC architecture is too abstract, and that it will add unneeded
overhead, especially those whose goal is to create a simple, arcade-style game.
Now, historically, arcade games were just that, games written for arcade
machines. The code ran "close to the metal", and would squeeze all the
resources of the machine just to get a 3-color ghost to flash blue every
other frame. In the 21st century, we have resource-rich personal computers
(and phones!) where applications run a couple layers above the metal. Hence,
organizing your code into a pattern has a small relative cost. For that small
cost, you get the following advantages: more easily add networking, easily add
new views (file loggers, radars, HUDs, multiple zoom levels, ...), keep the
Model code "cleaner" by decoupling it from the view and the controller, and
I contend, more readable code.
]