Skip to content
Navigation Menu
{{ message }}
forked from irskep/pyglettutorial
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTutorial.rtf
More file actions
1446 lines (1295 loc) · 48.2 KB
/
Copy pathTutorial.rtf
File metadata and controls
1446 lines (1295 loc) · 48.2 KB
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
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
{\rtf1\ansi\ansicpg1252\cocoartf949\cocoasubrtf460
{\fonttbl\f0\fnil\fcharset0 LucidaGrande;\f1\fnil\fcharset0 Monaco;}
{\colortbl;\red255\green255\blue255;}
\vieww10300\viewh15100\viewkind0
\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\ql\qnatural\pardirnatural
\f0\b\fs48 \cf0 Intro
\fs24 \
\
Why use Python for games?
\b0 \
The same reason you use Python for anything else. It's easy, it makes sense, and there are great libraries available.\
\
\b Speaking of libraries, what's available?
\b0 \
* PyGame\
* pyglet\
* Panda3D\
\
\b Which one should I use?\
\b0 My personal opinion is that pyglet is the cleanest and fastest, but PyGame has also been used to do some cool things. Panda3D is more sophisticated, geared toward 3D, and has a much higher learning curve. I won't go into specifics for now.\
\
To get you familiar with pyglet, I'll walk you through the process of creating a simple version of the classic game Asteroids.\
\
\b\fs48 Part 1: Basic Graphics
\b0\fs24 \
\
The first version of our Asteroids clone will simply show a score of zero, a label showing the name of the program, three randomly placed asteroids, and the player's ship. Nothing will move.
\b\fs48 \
\fs36 \
Setting Up
\fs24 \
\
Installing Pyglet\
\b0 Download pyglet from http://pyglet.org/download.html and choose the distribution for your platform. The process is different for each platform, but simple on all of them, since pyglet has no external dependencies.
\b \
\
Setting Up the Files\
\b0 Since I wrote this example in stages, I'm putting the folder with the images, called 'resources,' outside the example folders. Each example folder contains a Python file called
\f1\fs20 asteroid.py
\f0\fs24 which runs the game, as well as a
\f1\fs20 game
\f0\fs24 module which contains most of the functionality. Your folder structure should look like this:\
\
mygame/\
resources/\
(images go here)\
version1/\
asteroids.py\
game/\
__init__.py
\b \
\
Getting a Window\
\b0 To set up a window, simply import pyglet, create a new instance of
\f1\fs20 pyglet.window.Window
\f0\fs24 , and call
\f1\fs20 pyglet.app.run()
\f0\fs24 .\
\
\f1\fs20 import pyglet\
game_window = pyglet.window.Window(800, 600)\
\
if __name__ == '__main__':\
pyglet.app.run()
\f0\fs24 \
\
When you run the code above, you should see a window full of junk that goes away when you press Esc.\
\
\b Loading and Displaying an Image
\b0 \
Let's create a separate submodule of
\f1\fs20 game
\f0\fs24 to hold resources, calling it
\f1\fs20 resources.py
\f0\fs24 .\
\
Since our images reside in a directory other than the example's root directory, we need to tell pyglet where to find them:\
\
\f1\fs20 import pyglet\
pyglet.resource.path = ['../resources']\
pyglet.resource.reindex()\
\f0\fs24 \
The resource path starts with '../' because the
\f1\fs20 resources
\f0\fs24 folder is on the same level as the
\f1\fs20 version1
\f0\fs24 folder. If we left it off, pyglet would look inside
\f1\fs20 version1
\f0\fs24 for the
\f1\fs20 resources
\f0\fs24 folder.\
\
Now that pyglet's
\f1\fs20 resource
\f0\fs24 module is initialized, we can easily load the images:\
\
\f1\fs20 player_image = pyglet.resource.image("player.png")\
bullet_image = pyglet.resource.image("bullet.png")\
asteroid_image = pyglet.resource.image("asteroid.png")\
\f0\fs24 \
\b Centering the Images
\b0 \
Pyglet will draw all images from their lower left corner by default. We don't want this behavior for our images, which need to rotate around their centers. All we have to do to fix this problem is set their anchor points:\
\
\f1\fs20 def center_image(image):\
"""Sets an image's anchor point to its center"""\
image.anchor_x = image.width/2\
image.anchor_y = image.height/2
\f0\fs24 \
\
Now we can just call
\f1\fs20 center_image()
\f0\fs24 on all our loaded images:\
\
\f1\fs20 center_image(player_image)\
center_image(bullet_image)\
center_image(asteroid_image)
\f0\fs24 \
\
Remember that the
\f1\fs20 center_image()
\f0\fs24 function must be defined before it can be called at the module level. Also, note that zero degrees points directly to the right in pyglet, so the images are all drawn with their front pointing to the right.\
\
To access the images from
\f1\fs20 asteroids.py
\f0\fs24 , we need to use something like
\f1\fs20 from game import resources
\f0\fs24 , which we'll get into in the next section.
\b\fs48 \
\fs36 \
Initializing Objects
\b0\fs24 \
\
We want to put some labels at the top of the window to give the player some information about the score and the current level. Eventually, we will have a score display, the name of the level, and a row of icons representing the number of remaining lives.\
\
\b Making the Labels
\b0 \
To make a text label in pyglet, just initialize a
\f1\fs20 pyglet.text.Label
\f0\fs24 object:\
\
\f1\fs20 score_label = pyglet.text.Label(text="Score: 0", x=10, y=575)\
level_label = pyglet.text.Label(text="My Amazing Game", \
x=400, y=575, anchor_x='center')
\f0\fs24 \
\
Notice that the second label is centered using the anchor_x attribute.\
\
\b Drawing the Labels
\b0 \
We want pyglet to call a custom function whenever the window is drawn. To make that happen, we need to either subclass
\f1\fs20 Window
\f0\fs24 and override the
\f1\fs20 on_draw()
\f0\fs24 function, or use the
\f1\fs20 @Window.event
\f0\fs24 decorator on a function with the same name:\
\
\f1\fs20 @game_window.event\
def on_draw():\
# draw things here\
\f0\fs24 \
The
\f1\fs20 @game_window.event
\f0\fs24 decorator lets the Window instance know that
\f1\fs20 on_draw()
\f0\fs24 is an event handler. The on_draw event is fired whenever - you guessed it - the window needs to be redrawn. Other events include on_mouse_press and on_key_press.\
\
Now we can fill the method with the functions necessary to draw our labels. Before we draw anything, we should clear the screen. After that, we can simply call each object's
\f1\fs20 draw()
\f0\fs24 function.\
\
\f1\fs20 @game_window.event\
def on_draw():\
game_window.clear()\
\
level_label.draw()\
score_label.draw()
\f0\fs24 \
\
Now when you run
\f1\fs20 asteroids.py
\f0\fs24 , you should get a window with a score of zero in the upper left corner and a centered label reading "Version 1: Static Graphics" at the top of the screen.\
\
\b Making the Player and Asteroid Sprites\
\b0 The player should be an instance or subclass of
\f1\fs20 pyglet.sprite.Sprite
\f0\fs24 , like so:\
\
\f1\fs20 from game import resources\
...\
player_ship = pyglet.sprite.Sprite(img=resources.player_image, x=400, y=300)
\f0\fs24 \
\
To get the player to draw on the screen, add a line to
\f1\fs20 on_draw()
\f0\fs24 :\
\
\f1\fs20 @game_window.event\
def on_draw():\
...\
player_ship.draw()
\f0\fs24 \
\
Loading the asteroids is a little more complicated, since we'll need to place more than one at random locations that don't immediately collide with the player. Let's put the loading code in a new
\f1\fs20 game
\f0\fs24 submodule called
\f1\fs20 load.py
\f0\fs24 :\
\
\f1\fs20 import pyglet, random\
import resources\
\
def asteroids(num_asteroids):\
asteroids = []\
for i in range(num_asteroids):\
asteroid_x = random.randint(0, 800)\
asteroid_y = random.randint(0, 600)\
new_asteroid = pyglet.sprite.Sprite(img=resources.asteroid_image, \
x=asteroid_x, y=asteroid_y)\
new_asteroid.rotation = random.randint(0, 360)\
asteroids.append(new_asteroid)\
return asteroids
\f0\fs24 \
\
All we are doing here is making a few new sprites with random positions. There's still a problem, though: an asteroid might randomly be placed exactly where the player is, causing immediate death. To fix this issue, we'll need to be able to tell how far away new asteroids are from the player. Here is a simple function to calculate that distance:\
\
\f1\fs20 import math\
...\
def distance(point_1=(0, 0), point_2=(0, 0)):\
"""Returns the distance between two points"""\
return math.sqrt((point_1[0]-point_2[0])**2+(point_1[1]-point_2[1])**2)
\f0\fs24 \
\
To check new asteroids agains the player's position, we need to pass the player's position into the
\f1\fs20 asteroids()
\f0\fs24 function and keep regenerating new coordinates until the asteroid is far enough away. Pyglet sprites keep track of their position both as a tuple (
\f1\fs20 Sprite.position
\f0\fs24 ) and as
\f1\fs20 x
\f0\fs24 and
\f1\fs20 y
\f0\fs24 attributes (
\f1\fs20 Sprite.x
\f0\fs24 and
\f1\fs20 Sprite.y
\f0\fs24 ). To keep our code short, we'll just pass the position tuple into the function.\
\
\f1\fs20 def asteroids(num_asteroids, player_position):\
asteroids = []\
for i in range(num_asteroids):\
asteroid_x, asteroid_y = player_position\
while distance((asteroid_x, asteroid_y), player_position) < 100:\
asteroid_x = random.randint(0, 800)\
asteroid_y = random.randint(0, 600)\
new_asteroid = pyglet.sprite.Sprite(img=resources.asteroid_image, \
x=asteroid_x, y=asteroid_y)\
new_asteroid.rotation = random.randint(0, 360)\
asteroids.append(new_asteroid)\
return asteroids
\f0\fs24 \
\
For each asteroid, it chooses random positions until it finds one away from the player, creates the sprite, and gives it a random rotation. Each asteroid is appended to a list, which is returned.\
\
Now you can load three asteroids like this:\
\
\f1\fs20 from game import resources, load\
...\
asteroids = load.asteroids(3, player_ship.position)
\f0\fs24 \
\
The
\f1\fs20 asteroids
\f0\fs24 variable now contains a list of sprites. Drawing them on the screen is as simple as it was for the player's ship: just cal their
\f1\fs20 draw()
\f0\fs24 methods.\
\
\f1\fs20 @game_window.event\
def on_draw():\
...\
for asteroid in asteroids:\
asteroid.draw()
\f0\fs24 \
\
\b\fs48 Part 2: Basic Motion
\fs24 \
\
\b0 In the second version of the example, we'll introduce a simpler, faster way to draw all of the game objects, as well as add row of icons indicating the number of lives left. We'll also write some code to make the player and the asteroids obey the laws of physics.
\b\fs48 \
\fs36 \
More Graphics
\fs24 \
\
Drawing with Batches\
\b0 Calling each object's draw() method manually can become cumbersome and tedious if there are many different kinds of objects. Graphics batches simplify drawing by letting you draw all your objects with a single function call. All you need to do is create a batch, pass it into each object you want to draw, and call the batch's
\f1\fs20 draw()
\f0\fs24 method.\
\
To create a new batch, simply call
\f1\fs20 pyglet.graphics.Batch()
\f0\fs24 :\
\
\f1\fs20 main_batch = pyglet.graphics.Batch()
\f0\fs24 \
\
To make an object a member of a batch, just pass the batch into its constructor as the
\f1\fs20 batch
\f0\fs24 keyword argument:\
\
\f1\fs20 score_label = pyglet.text.Label(text="Score: 0", x=10, y=575, batch=main_batch)
\f0\fs24 \
\
Add the
\f1\fs20 batch
\f0\fs24 keyword argument to each graphical object created in
\f1\fs20 asteroids.py
\f0\fs24 .\
\
To use the batch with the asteroid sprites, we'll need to pass the batch into the
\f1\fs20 game.load.asteroid()
\f0\fs24 function, then just add it as a keyword argument to each new sprite:\
\
\f1\fs20 def asteroids(num_asteroids, player_position, batch=None):\
...\
new_asteroid = pyglet.sprite.Sprite(img=resources.asteroid_image, \
x=asteroid_x, y=asteroid_y,\
batch=batch)\
\f0\fs24 \
\f1\fs20 asteroids = load.asteroids(3, player_ship.position, main_batch)
\f0\fs24 \
\
Now you can replace those five lines of
\f1\fs20 draw()
\f0\fs24 calls with just one:\
\
\f1\fs20 main_batch.draw()
\f0\fs24 \
\
Now when you run
\f1\fs20 asteroids.py
\f0\fs24 , it should look exactly the same.\
\
\b Displaying Little Ship Icons
\b0 \
To show how many lives the player has left, we'll need to draw a little row of icons in the upper right corner of the screen. Since we'll be making more than one using the same template, let's create a function called
\f1\fs20 player_lives()
\f0\fs24 in the
\f1\fs20 load
\f0\fs24 module to generate them.\
\
The icons should look the same as the player's ship. We could create a scaled version using an image editor, or we could just let pyglet do the scaling. I don't know about you, but I prefer the option that requires less work.\
\
The function for creating the icons is almost exactly the same as the one for creating asteroids. For each icon we just create a sprite, give it a position and scale, and append it to the return list.\
\
\f1\fs20 def player_lives(num_icons, batch=None):\
player_lives = []\
for i in range(num_icons):\
new_sprite = pyglet.sprite.Sprite(img=resources.player_image, \
x=785-i*30, y=585, \
batch=batch)\
new_sprite.scale = 0.5\
player_lives.append(new_sprite)\
return player_lives
\f0\fs24 \
\
The player icon is 50x50 pixels, so half that size will be 25x25. We want to put a little bit of space between each icon, so we create them at 30-pixel intervals starting from the right side of the screen and moving to the left. Note that like the
\f1\fs20 asteroids()
\f0\fs24 function,
\f1\fs20 player_lives()
\f0\fs24 takes a
\f1\fs20 batch
\f0\fs24 argument. A
\f1\fs20 None
\f0\fs24 value specifies no batch.\
\b\fs36 \
Making Things Move
\b0\fs24 \
\
The game would be pretty boring if nothing on the screen ever moved. To achieve motion, we'll need to write our own set of classes to handle frame-by-frame movement calculations. We'll also need to write a Player class to respond to keyboard input.\
\
\b Creating the Basic Motion Class\
\b0 Since every visible object is represented by at least one Sprite, we may as well make our basic motion class a subclass of
\f1\fs20 pyglet.sprite.Sprite
\f0\fs24 . Another approach would be to have our class inherit from
\f1\fs20 object
\f0\fs24 and have a
\f1\fs20 sprite
\f0\fs24 attribute, but I find that simply subclassing
\f1\fs20 Sprite
\f0\fs24 provides more convenient notation.\
\
Create a new
\f1\fs20 game
\f0\fs24 submodule called
\f1\fs20 physicalobject.py
\f0\fs24 and declare a
\f1\fs20 PhysicalObject
\f0\fs24 class. The only new attributes we'll be adding will store the object's velocity, so the constructor will be simple.\
\
\f1\fs20 class PhysicalObject(pyglet.sprite.Sprite):\
\
def __init__(self, *args, **kwargs):\
super(PhysicalObject, self).__init__(*args, **kwargs)\
\
self.velocity_x, self.velocity_y = 0.0, 0.0
\f0\fs24 \
\
Each object will need to be updated every frame, so let's write an
\f1\fs20 update()
\f0\fs24 method.\
\
\f1\fs20 def update(self, dt):\
self.x += self.velocity_x * dt\
self.y += self.velocity_y * dt
\f0\fs24 \
\
What's
\f1\fs20 dt
\f0\fs24 ? It's the
\b time step
\b0 . Game frames are not instantaneous, and they don't always take equal amounts of time. If you've ever tried to play a modern game on an old machine, you know that frame rates can jump all over the place. There are a number of ways to deal with this problem, the simplest one being to just multiply all time-sensitive operations by
\f1\fs20 dt
\f0\fs24 .\
\
If we give objects a velocity and just let them go, they will fly off the screen before long. Since we're making a version of Asteroids, we would rather they just wrapped around the screen. Here is a simple function that accomplishes this goal:\
\
\f1\fs20 def check_bounds(self):\
min_x = -self.image.width/2\
min_y = -self.image.height/2\
max_x = 800 + self.image.width/2\
max_y = 600 + self.image.height/2\
if self.x < min_x:\
self.x = max_x\
elif self.x > max_x:\
self.x = min_x\
if self.y < min_y:\
self.y = max_y\
elif self.y > max_y:\
self.y = min_y
\f0\fs24 \
\
As you can see, it simply checks to see if objects are no longer visible on the screen, and if so, it moves them to the other side of the screen. To make every PhysicalObject use this behavior, add a call to
\f1\fs20 self.check_bounds()
\f0\fs24 at the end of
\f1\fs20 update()
\f0\fs24 .\
\
To make the asteroids use our new motion code, just import the
\f1\fs20 physicalobject
\f0\fs24 module and change the "new_asteroid = ..." line to create a new
\f1\fs20 PhysicalObject
\f0\fs24 instead of a
\f1\fs20 Sprite
\f0\fs24 . You'll also want to give them a random initial velocity. Here is the new, improved
\f1\fs20 load.asteroids() function
\f0\fs24 :\
\
\f1\fs20 def asteroids(num_asteroids, player_position, batch=None):\
...\
new_asteroid = physicalobject.PhysicalObject(...)\
new_asteroid.rotation = random.randint(0, 360)\
new_asteroid.velocity_x = random.random()*40\
new_asteroid.velocity_y = random.random()*40\
...
\f0\fs24 \
\
\b Writing the Game Update Function
\b0 \
To call each object's
\f1\fs20 update()
\f0\fs24 method every frame, we first need to have a list of those objects. For now, we can just declare it after setting up all the other objects:\
\
\f1\fs20 game_objects = [player_ship] + asteroids
\f0\fs24 \
\
Now we can write a simple function to iterate over the list:\
\
\f1\fs20 def update(dt):\
for obj in game_objects:\
obj.update(dt)
\f0\fs24 \
\
The
\f1\fs20 update()
\f0\fs24 function takes a
\f1\fs20 dt
\f0\fs24 parameter because it is still not the source of the actual time step.\
\
\b Calling the Update Function
\b0 \
We need to update the objects at least once per frame. What's a frame? Well, most screens have a maximum refresh rate of 60 hertz. If we set our loop to run at exactly 60 hertz, though, the motion will look a little jerky because it won't match the screen exactly. Instead, we should have it update twice as fast, 120 times per second, to get smooth animation.\
\
Instead of using an actual loop to update the game every frame, we let pyglet call the function at a specified interval, using no more resources than are necessary. The
\f1\fs20 pyglet.clock
\f0\fs24 module contains a number of ways to call functions periodically or at some specified time in the future. The one we want is
\f1\fs20 pyglet.clock.schedule_interval()
\f0\fs24 :\
\
\f1\fs20 pyglet.clock.schedule_interval(update, 1/120.0)
\f0\fs24 \
\
Putting this line above
\f1\fs20 pyglet.app.run()
\f0\fs24 in the
\f1\fs20 if __name__ == '__main__'
\f0\fs24 block tells pyglet to call
\f1\fs20 update()
\f0\fs24 120 times per second. Pyglet will pass in the elapsed time, i.e.
\f1\fs20 dt
\f0\fs24 , as the only parameter.\
\
Now when you run
\f1\fs20 asteroids.py
\f0\fs24 , you should see your formerly static asteroids drifting serenely across the screen, reappearing on the other side when they slide off the edge.\
\
\b Writing the Player Class
\b0 \
In addition to obeying the basic laws of physics, the player object needs to respond to keyboard input. Start by creating a
\f1\fs20 game.player
\f0\fs24 module, importing the appropriate modules, and subclassing
\f1\fs20 PhysicalObject
\f0\fs24 :\
\
\f1\fs20 import physicalobject, resources\
\
class Player(physicalobject.PhysicalObject):\
\
def __init__(self, *args, **kwargs):\
super(Player, self).__init__(img=resources.player_image, \
*args, **kwargs)\
\f0\fs24 \
So far, the only difference between a
\f1\fs20 Player
\f0\fs24 and a
\f1\fs20 PhysicalObject
\f0\fs24 is that a
\f1\fs20 Player
\f0\fs24 will always have the same image. But
\f1\fs20 Player
\f0\fs24 objects need a couple more attributes. Since the ship will always thrust with the same force in whatever direction it points, we'll need to define a constant for the magnitude of that force. We should also define a constant for the ship's rotation speed.\
\
\f1\fs20 self.thrust = 300.0\
self.rotate_speed = 200.0
\f0\fs24 \
\
Now we need to get the class to respond to user input. Pyglet uses a polling approach to input, sending key press and key release events to registered event handlers. We will need to constantly check if a key is down, and one way to accomplish that is to maintain a dictionary of keys. First, we need to initialize the dictionary in the constructor:\
\
\f1\fs20 self.keys = dict(left=False, right=False, up=False)
\f0\fs24 \
\
Then we need to write two methods,
\f1\fs20 on_key_press()
\f0\fs24 and
\f1\fs20 on_key_release()
\f0\fs24 . When pyglet checks a new event handler, it looks for these two methods, among others.\
\
\f1\fs20 import math\
from pyglet.window import key\
import physicalobject, resources\
...\
class Player(physicalobject.PhysicalObject)\
...\
def on_key_press(self, symbol, modifiers): \
if symbol == key.UP:\
self.keys['up'] = True\
elif symbol == key.LEFT:\
self.keys['left'] = True\
elif symbol == key.RIGHT:\
self.keys['right'] = True\
\
def on_key_release(self, symbol, modifiers):\
if symbol == key.UP:\
self.keys['up'] = False\
elif symbol == key.LEFT:\
self.keys['left'] = False\
elif symbol == key.RIGHT:\
self.keys['right'] = False
\f0\fs24 \
\
That looks pretty cumbersome. There's a better way to do it which we'll see later, but for now, this version serves as a good demonstration of pyglet's event system.\
\
The last thing we need to do is write the
\f1\fs20 update()
\f0\fs24 method. It follows the same behavior as a
\f1\fs20 PhysicalObject
\f0\fs24 plus a little extra, so we'll need to call
\f1\fs20 PhysicalObject
\f0\fs24 's
\f1\fs20 update()
\f0\fs24 method and then respond to input.\
\
\f1\fs20 def update(self, dt):\
super(Player, self).update(dt)\
\
if self.keys['left']:\
self.rotation -= self.rotate_speed * dt\
if self.keys['right']:\
self.rotation += self.rotate_speed * dt
\f0\fs24 \
\
Pretty simple so far. To rotate the player, we just add the rotation speed to the angle, multiplied by
\f1\fs20 dt
\f0\fs24 to account for time. Note that
\f1\fs20 Sprite
\f0\fs24 objects' rotation attributes are in degrees, with clockwise as the positive direction. This means that you need to call
\f1\fs20 math.degrees()
\f0\fs24 or
\f1\fs20 math.radians()
\f0\fs24 and make the result negative whenever you use Python's built-in math functions with the
\f1\fs20 Sprite
\f0\fs24 class, since those functions use radians instead of degrees, and their positive direction is counter-clockwise. The code to make the ship thrust forward uses an example of such a conversion:\
\
\f1\fs20 if self.keys['up']:\
angle_radians = -math.radians(self.rotation)\
force_x = math.cos(angle_radians) * self.thrust * dt\
force_y = math.sin(angle_radians) * self.thrust * dt\
self.velocity_x += force_x\
self.velocity_y += force_y
\f0\fs24 \
\
First, we convert the angle to radians so that
\f1\fs20 math.cos()
\f0\fs24 and
\f1\fs20 math.sin()
\f0\fs24 will get the correct values. Then we apply some simple physics to modify the ship's X and Y velocity components and push the ship in the right direction.\
\
We now have a complete
\f1\fs20 Player
\f0\fs24 class. If we add it to the game and tell pyglet that it's an event handler, we should be good to go.\
\
\b Integrating the Player Class\
\b0 The first thing we need to do is make
\f1\fs20 player_ship
\f0\fs24 an instance of
\f1\fs20 Player
\f0\fs24 :\
\
\f1\fs20 from game import player\
...\
player_ship = player.Player(x=400, y=300, batch=main_batch)
\f0\fs24 \
\
Now we need to tell pyglet that
\f1\fs20 player_ship
\f0\fs24 is an event handler. To do that, we need to push it onto the event stack with
\f1\fs20 game_window.push_handlers()
\f0\fs24 :\
\
\f1\fs20 game_window.push_handlers(player_ship)
\f0\fs24 \
\
That's it! Now you should be able to run the game and move the player with the arrow keys.\
\
\b\fs48 Part 3: Giving the Player Something to Do
\fs24 \
\
\b0 In any good game, there needs to be something working against the player. In the case of Asteroids, it's the threat of collision with, well, an asteroid. Collision detection requires a lot of infrastructure in the code, so this section will focus on making it work. We'll also clean up the player class and show some visual feedback for thrusting.
\b\fs48 \
\fs36 \
Simplifying Player Input
\fs24 \
\
\b0 Right now, the
\f1\fs20 Player
\f0\fs24 class handles all of its own keyboard events. It spends 13 lines of code doing nothing but setting boolean values in a dictionary. One would think that there would be a better way, and there is:
\f1\fs20 pyglet.window.key.KeyStateHandler
\f0\fs24 . This handy class automatically does what we have been doing manually: it tracks the state of every key on the keyboard.\
\
To start using it, we need to initialize it and push it onto the event stack instead of the
\f1\fs20 Player
\f0\fs24 class. First, let's add it to
\f1\fs20 Player
\f0\fs24 's constructor:\
\
\f1\fs20 self.key_handler = key.KeyStateHandler()
\f0\fs24 \
\
We also need to push the
\f1\fs20 key_handler
\f0\fs24 object onto the event stack. Keep pushing the
\f1\fs20 player_ship
\f0\fs24 object in addition to its key handler, because we'll need it to keep handling key press and release events later.\
\
\f1\fs20 game_window.push_handlers(player_ship.key_handler)
\f0\fs24 \
\
Since
\f1\fs20 Player
\f0\fs24 now relies on
\f1\fs20 key_handler
\f0\fs24 to read the keyboard, we need to change the
\f1\fs20 update()
\f0\fs24 method to use it. The only changes are in the
\f1\fs20 if
\f0\fs24 conditions:\
\
\f1\fs20 if self.key_handler[key.LEFT]:\
...\
if self.key_handler[key.RIGHT]:\
...\
\
if self.key_handler[key.UP]:\
...
\f0\fs24 \
\
Now we can remove the
\f1\fs20 on_key_press()
\f0\fs24 and
\f1\fs20 on_key_release()
\f0\fs24 methods from the class. It's just that simple. If you need to see a list of key constants, you can check the API documentation under
\f1\fs20 pyglet.window.key
\f0\fs24 .\
\
\b\fs36 Adding an Engine Flame
\fs24 \
\
\b0 Without visual feedback, it can be difficult to tell if the ship is actually thrusting forward or not, especially for an observer just watching someone else play the game. One way to provide visual feedback is to show an engine flame behind the player while the player is thrusting.\
\
\b Loading the Flame Image\
\b0 The player will now be made of two sprites. There's nothing preventing us from letting a
\f1\fs20 Sprite
\f0\fs24 own another
\f1\fs20 Sprite
\f0\fs24 , so we'll just give
\f1\fs20 Player
\f0\fs24 an
\f1\fs20 engine_sprite
\f0\fs24 attribute and update it every frame. For our purposes, this approach will be the simplest and most scalable.\
\
To make the flame draw in the correct position, we could either do some complicated math every frame, or we could just move the image's anchor point. First, load the image in
\f1\fs20 resources.py
\f0\fs24 :\
\
\f1\fs20 engine_image = pyglet.resource.image("engine_flame.png")
\f0\fs24 \
\
To get the flame to draw behind the player, we need to move the flame image's center of rotation to the right, past the end of the image. To do that, we just set its
\f1\fs20 anchor_x
\f0\fs24 and
\f1\fs20 anchor_y
\f0\fs24 attributes:\
\
\f1\fs20 engine_image.anchor_x = engine_image.width * 1.5\
engine_image.anchor_y = engine_image.height / 2
\f0\fs24 \
\
Now the image is ready to be used by the player class. If you're still confused about anchor points, experiment with the values for
\f1\fs20 engine_image
\f0\fs24 's anchor point when you finish this section.\
\
\b Creating and Drawing the Flame\
\b0 The engine sprite needs to be initialized with all the same arguments as
\f1\fs20 Player
\f0\fs24 , except that it needs a different image and must be initially invisible. The code for creating it belongs in
\f1\fs20 Player.__init__()
\f0\fs24 and is very straightforward:\
\
\f1\fs20 self.engine_sprite = pyglet.sprite.Sprite(img=resources.engine_image, \
*args, **kwargs)\
self.engine_sprite.visible = False
\f0\fs24 \
\
To make the engine sprite appear only while the player is thrusting, we need to add some logic to the if
\f1\fs20 self.key_handler[key.UP]
\f0\fs24 block in the
\f1\fs20 update()
\f0\fs24 method.\
\
\f1\fs20 if self.key_handler[key.UP]:\
...\
self.engine_sprite.visible = True\
else:\
self.engine_sprite.visible = False
\f0\fs24 \
\
To make the sprite appear at the player's position, we also need to update its position and rotation attributes:\
\
\f1\fs20 if self.key_handler[key.UP]:\
...\
self.engine_sprite.rotation = self.rotation\
self.engine_sprite.x = self.x\
self.engine_sprite.y = self.y\
self.engine_sprite.visible = True\
else:\
self.engine_sprite.visible = False
\f0\fs24 \
\
\b Cleaning Up After Death\
\b0 When the player is inevitably smashed to bits by an asteroid, he will disappear from the screen. However, simply removing the
\f1\fs20 Player
\f0\fs24 instance from the
\f1\fs20 game_objects
\f0\fs24 list is not enough for it to be removed from the graphics batch. To do that, we need to call its
\f1\fs20 delete()
\f0\fs24 method. Normally a
\f1\fs20 Sprite
\f0\fs24 's own
\f1\fs20 delete()
\f0\fs24 method will work fine without modifications, but our subclass has its own
\f1\fs20 Sprite
\f0\fs24 which must also be deleted when the
\f1\fs20 Player
\f0\fs24 instance is deleted. To get both to die gracefully, we must write a simple
\f1\fs20 delete()
\f0\fs24 method:\
\
\f1\fs20 def delete(self):\
self.engine_sprite.delete()\
super(Player, self).delete()
\f0\fs24 \
\
The
\f1\fs20 Player
\f0\fs24 class is now cleaned up and ready to go.\
\
\b\fs36 Checking For Collisions
\fs24 \
\
\b0 To make objects disappear from the screen, we'll need to manipulate the
\f1\fs20 game_objects
\f0\fs24 list. Every object will need to check every other object's position against its own, and each object will have to decide whether or not it should be removed from the list. The game loop will then check for dead objects and remove them from the list.\
\
\b Checking All Object Pairs\
\b0 We need to check every object against every other object. The simplest method is to use nested loops. This method will be inefficient for a large number of objects*, but it will work for our purposes. We can use one easy optimization and avoid checking the same pair of objects twice. Here's the setup for the loops, which belongs in
\f1\fs20 update()
\f0\fs24 . It simply iterates over all object pairs without doing anything.\
\
* A better technique is spatial hashing, but if you reach the point where you need to use that, you should probably be using a C-based physics engine anyway.\
\
\f1\fs20 for i in xrange(len(game_objects)):\
for j in xrange(i+1, len(game_objects)):\
obj_1 = game_objects[i]\
obj_2 = game_objects[j]
\f0\fs24 \
\
We'll need a way to check if an object has already been killed. We could go over to
\f1\fs20 PhysicalObject
\f0\fs24 right now and put it in, but let's keep working on the game loop and implement the method later. For now, we'll just assume that everything in
\f1\fs20 game_objects
\f0\fs24 has a
\f1\fs20 dead
\f0\fs24 attribute which will be
\f1\fs20 False
\f0\fs24 until the class sets it to
\f1\fs20 True
\f0\fs24 , at which point it will be ignored and eventually removed from the list.\
\
To perform the actual check, we'll also need to call two more methods that don't exist yet. One method will determine if the two objects actually collide, and the other method will give each object an opportunity to respond to the collision. The checking code itself is easy to understand, so I won't bother you with further explanations:\
\
\f1\fs20 if not obj_1.dead and not obj_2.dead:\
if obj_1.collides_with(obj_2):\
obj_1.handle_collision_with(obj_2)\
obj_2.handle_collision_with(obj_1)
\f0\fs24 \
\
Now all that remains is for us to go through the list and remove dead objects:\
\
\f1\fs20 ...update game objects...\
\
for to_remove in [obj for obj in game_objects if obj.dead]:\
to_remove.delete()\
game_objects.remove(to_remove)
\f0\fs24 \
\
As you can see, it simply calls the object's
\f1\fs20 delete()
\f0\fs24 method to remove it from any batches, then it removes it from the list. If you haven't used list comprehensions much, the above code might look like it's removing objects from the list while traversing it. Fortunately, the list comprehension is evaluated before the loop actually runs, so there should be no problems.\
\
\b Implementing the Collision Functions
\b0 \
We need to add three things to the
\f1\fs20 PhysicalObject
\f0\fs24 class: the
\f1\fs20 dead
\f0\fs24 attribute, the
\f1\fs20 collides_with()
\f0\fs24 method, and the
\f1\fs20 handle_collision_with()
\f0\fs24 method. The
\f1\fs20 collides_with()
\f0\fs24 method will need to use the
\f1\fs20 distance()
\f0\fs24 function, so let's start by moving that function into its own submodule of
\f1\fs20 game
\f0\fs24 , called
\f1\fs20 util.py
\f0\fs24 :\
\
\f1\fs20 import pyglet, math\
\
def distance(point_1=(0, 0), point_2=(0, 0)):\
return math.sqrt((point_1[0]-point_2[0])**2+(point_1[1]-point_2[1])**2)
\f0\fs24 \
\
Remember to call
\f1\fs20 from util import distance
\f0\fs24 in
\f1\fs20 load.py
\f0\fs24 . Now we can write
\f1\fs20 PhysicalObject.collides_with()
You can’t perform that action at this time.
