4.2. Pong (obj)
Klasyczna gra w odbijanie piłeczki zrealizowana z użyciem biblioteki PyGame. Wersja obiektowa. Biblioteka PyGame ułatwia tworzenie aplikacji multimedialnych, w tym gier.

4.2.1. Przygotowanie
Do rozpoczęcia pracy z przykładem pobieramy szczątkowy kod źródłowy:
~/python101$ git checkout -f pong/z1
4.2.2. Okienko gry
Na wstępie w pliku ~/python101/games/pong.py
otrzymujemy kod który przygotuje okienko naszej gry:
1# coding=utf-8
2
3import pygame
4import pygame.locals
5
6
7class Board(object):
8 """
9 Plansza do gry. Odpowiada za rysowanie okna gry.
10 """
11
12 def __init__(self, width, height):
13 """
14 Konstruktor planszy do gry. Przygotowuje okienko gry.
15
16 :param width:
17 :param height:
18 """
19 self.surface = pygame.display.set_mode((width, height), 0, 32)
20 pygame.display.set_caption('Simple Pong')
21
22 def draw(self, *args):
23 """
24 Rysuje okno gry
25
26 :param args: lista obiektów do narysowania
27 """
28 background = (230, 255, 255)
29 self.surface.fill(background)
30 for drawable in args:
31 drawable.draw_on(self.surface)
32
33 # dopiero w tym miejscu następuje fatyczne rysowanie
34 # w oknie gry, wcześniej tylko ustalaliśmy co i jak ma zostać narysowane
35 pygame.display.update()
36
37
38board = Board(800, 400)
39board.draw()
W powyższym kodzie zdefiniowaliśmy klasę Board
z dwiema metodami:
konstruktorem
__init__
, orazmetodą
draw
posługującą się bibliotekąPyGame
do rysowania w oknie.
Na końcu utworzyliśmy instancję klasy Board
i wywołaliśmy jej metodę draw
na razie
bez żadnych elementów wymagających narysowania.
Informacja
Każdy plik skryptu Python jest uruchamiany w momencie importu — plik/moduł główny jest importowany jako pierwszy.
Deklaracje klas są faktycznie instrukcjami sterującymi mówiącymi, by w aktualnym module utworzyć typy zawierające wskazane definicje.
Możemy mieszać deklaracje klas ze zwykłymi instrukcjami sterującymi takimi jak print
,
czy przypisaniem wartości zmiennej board = Board(800, 400)
i następnie wywołaniem
metody na obiekcie board.draw()
.
Nasz program możemy uruchomić komendą:
~/python101$ python games/pong.py
Mrugnęło? Program się wykonał i zakończył działanie :). Żeby zobaczyć efekt na dłużej, możemy na końcu chwilkę uśpić nasz program:
39import time
40time.sleep(5)
Jednak zamiast tego, dla lepszej kontroli powinniśmy zadeklarować klasę kontrolera gry, usuńmy kod od linii 37 do końca i dodajmy klasę kontrolera:
38class PongGame(object):
39 """
40 Łączy wszystkie elementy gry w całość.
41 """
42
43 def __init__(self, width, height):
44 pygame.init()
45 self.board = Board(width, height)
46 # zegar którego użyjemy do kontrolowania szybkości rysowania
47 # kolejnych klatek gry
48 self.fps_clock = pygame.time.Clock()
49
50 def run(self):
51 """
52 Główna pętla programu
53 """
54 while not self.handle_events():
55 # działaj w pętli do momentu otrzymania sygnału do wyjścia
56 self.board.draw()
57 self.fps_clock.tick(30)
58
59 def handle_events(self):
60 """
61 Obsługa zdarzeń systemowych, tutaj zinterpretujemy np. ruchy myszką
62
63 :return True jeżeli pygame przekazał zdarzenie wyjścia z gry
64 """
65 for event in pygame.event.get():
66 if event.type == pygame.locals.QUIT:
67 pygame.quit()
68 return True
69
70
71# Ta część powinna być zawsze na końcu modułu (ten plik jest modułem)
72# chcemy uruchomić naszą grę dopiero po tym jak wszystkie klasy zostaną zadeklarowane
73if __name__ == "__main__":
74 game = PongGame(800, 400)
75 game.run()
Informacja
Prócz dodania kontrolera zmieniliśmy także sposób, w jaki gra jest uruchamiana — nie mylić z uruchomieniem programu.
Na końcu dodaliśmy instrukcję warunkową
if __name__ == "__main__":
, w niej sprawdzamy, czy nasz moduł jest modułem
głównym programu, jeśli nim jest, gra zostanie uruchomiona.
Dzięki temu, jeśli nasz moduł zostałby zaimportowany gdzieś indziej instrukcją
import pong
, deklaracje klas wykonałyby się, ale sama gra nie zostanie
uruchomiona.
Gotowy kod możemy pobrać komendą:
~/python101$ git checkout -f pong/z2
4.2.3. Piłeczka
Czas dodać piłkę do gry. Piłeczką będzie kolorowe kółko, które z każdym przejściem naszej pętli przesuniemy o kilka punktów w osi X i Y, zgodnie wektorem prędkości.
Wcześniej jednak zdefiniujemy wspólną klasę bazową dla obiektów, które będziemy rysować w oknie naszej gry:
71class Drawable(object):
72 """
73 Klasa bazowa dla rysowanych obiektów
74 """
75
76 def __init__(self, width, height, x, y, color=(0, 255, 0)):
77 self.width = width
78 self.height = height
79 self.color = color
80 self.surface = pygame.Surface([width, height], pygame.SRCALPHA, 32).convert_alpha()
81 self.rect = self.surface.get_rect(x=x, y=y)
82
83 def draw_on(self, surface):
84 surface.blit(self.surface, self.rect)
Następnie dodajmy klasę samej piłeczki dziedzicząc z Drawable
:
87class Ball(Drawable):
88 """
89 Piłeczka, sama kontroluje swoją prędkość i kierunek poruszania się.
90 """
91 def __init__(self, width, height, x, y, color=(255, 0, 0), x_speed=3, y_speed=3):
92 super(Ball, self).__init__(width, height, x, y, color)
93 pygame.draw.ellipse(self.surface, self.color, [0, 0, self.width, self.height])
94 self.x_speed = x_speed
95 self.y_speed = y_speed
96 self.start_x = x
97 self.start_y = y
98
99 def bounce_y(self):
100 """
101 Odwraca wektor prędkości w osi Y
102 """
103 self.y_speed *= -1
104
105 def bounce_x(self):
106 """
107 Odwraca wektor prędkości w osi X
108 """
109 self.x_speed *= -1
110
111 def reset(self):
112 """
113 Ustawia piłeczkę w położeniu początkowym i odwraca wektor prędkości w osi Y
114 """
115 self.rect.move(self.start_x, self.start_y)
116 self.bounce_y()
117
118 def move(self):
119 """
120 Przesuwa piłeczkę o wektor prędkości
121 """
122 self.rect.x += self.x_speed
123 self.rect.y += self.y_speed
W przykładzie powyżej wykonaliśmy dziedziczenie oraz przesłanianie konstruktora,
ponieważ rozszerzamy Drawable
i chcemy zachować efekt działania konstruktora na początku
konstruktora Ball
wywołujemy konstruktor klasy bazowej:
super(Ball, self).__init__(width, height, x, y, color)
Teraz musimy naszą piłeczkę zintegrować z resztą gry:
38class PongGame(object):
39 """
40 Łączy wszystkie elementy gry w całość.
41 """
42
43 def __init__(self, width, height):
44 pygame.init()
45 self.board = Board(width, height)
46 # zegar którego użyjemy do kontrolowania szybkości rysowania
47 # kolejnych klatek gry
48 self.fps_clock = pygame.time.Clock()
49 self.ball = Ball(20, 20, width/2, height/2)
50
51 def run(self):
52 """
53 Główna pętla programu
54 """
55 while not self.handle_events():
56 # działaj w pętli do momentu otrzymania sygnału do wyjścia
57 self.ball.move()
58 self.board.draw(
59 self.ball,
60 )
61 self.fps_clock.tick(30)
Informacja
Metoda Board.draw
oczekuje wielu opcjonalnych argumentów, choć na razie przekazujemy
tylko jeden. By zwiększyć czytelność potencjalnie dużej listy argumentów — kto
wie co jeszcze dodamy :) — podajemy każdy argument w osobnej linii zakończonej przecinkiem ,
.
Python nie traktuje takich osieroconych przecinków jako błąd, jest to ukłon w stronę programistów, którzy często zmieniają kod, kopiują i wklejają kawałki.
Dzięki temu możemy wstawiać nowe i zmieniać kolejność bez zwracania uwagi, czy na końcu jest przecinek, czy go brakuje, czy go należy usunąć. Zgodnie z konwencją powinien być tam zawsze.
Gotowy kod możemy pobrać komendą:
~/python101$ git checkout -f pong/z3
4.2.4. Odbijanie piłeczki
Uruchommy naszą „grę” ;)
~/python101$ python games/pong.py

Efekt nie jest powalający, ale mamy już jakiś ruch na planszy. Szkoda, że piłka spada z planszy. Może mogła by się odbijać od krawędzi okienka? Możemy wykorzystać wcześniej przygotowane metody do zmiany kierunku wektora prędkości, musimy tylko wykryć moment w którym piłeczka będzie dotykać krawędzi.
W tym celu piłeczka musi być świadoma istnienia planszy i pozycji krawędzi, dlatego
zmodyfikujemy metodę Ball.move
tak by przyjmowała board
jako argument i na
jego podstawie sprawdzimy, czy piłeczka powinna się odbijać:
122def move(self, board):
123 """
124 Przesuwa piłeczkę o wektor prędkości
125 """
126 self.rect.x += self.x_speed
127 self.rect.y += self.y_speed
128
129 if self.rect.x < 0 or self.rect.x > board.surface.get_width():
130 self.bounce_x()
131
132 if self.rect.y < 0 or self.rect.y > board.surface.get_height():
133 self.bounce_y()
Jeszcze zmodyfikujmy wywołanie metody move
w naszej pętli głównej:
51def run(self):
52 """
53 Główna pętla programu
54 """
55 while not self.handle_events():
56 self.ball.move(self.board)
57 self.board.draw(
58 self.ball,
59 )
60 self.fps_clock.tick(30)
Ostrzeżenie
Powyższe przykłady mają o jedno wcięcie za mało. Poprawnie wcięte przykłady straciłyby kolorowanie w tej formie materiałów. Ze względu na czytelność kodu zdecydowaliśmy się na taki drobny błąd. Kod po ewentualnym wklejeniu należy poprawić dodając jedno wcięcie (4 spacje).
Sprawdzamy, czy piłka się odbija, uruchamiamy nasz program:
~/python101$ python games/pong.py
Gotowy kod możemy pobrać komendą:
~/python101$ git checkout -f pong/z4
4.2.5. Odbijamy piłeczkę rakietką
Dodajmy „rakietkę”, przy pomocy której będziemy mogli odbijać piłeczkę. Będzie to zwykły prostokąt, który będziemy przesuwać za pomocą myszki.
136class Racket(Drawable):
137 """
138 Rakietka, porusza się w osi X z ograniczeniem prędkości.
139 """
140
141 def __init__(self, width, height, x, y, color=(0, 255, 0), max_speed=10):
142 super(Racket, self).__init__(width, height, x, y, color)
143 self.max_speed = max_speed
144 self.surface.fill(color)
145
146 def move(self, x):
147 """
148 Przesuwa rakietkę w wyznaczone miejsce.
149 """
150 delta = x - self.rect.x
151 if abs(delta) > self.max_speed:
152 delta = self.max_speed if delta > 0 else -self.max_speed
153 self.rect.x += delta
Informacja
W tym przykładzie zastosowaliśmy operator warunkowy, który ogranicza prędkość poruszania się rakietki:
delta = self.max_speed if delta > 0 else -self.max_speed
Zmienna delta
otrzyma wartość max_speed
ze znakiem +
lub -
w zależności od znaku jaki ma aktualnie.
Następnie „pokażemy” rakietkę piłeczce, tak by mogła się od niej odbijać.
Wiemy że rakietek będzie więcej, dlatego od razu tak zmodyfikujemy metodę
Ball.move
, by przyjmowała kolekcję rakietek:
122def move(self, board, *args):
123 """
124 Przesuwa piłeczkę o wektor prędkości
125 """
126 self.rect.x += self.x_speed
127 self.rect.y += self.y_speed
128
129 if self.rect.x < 0 or self.rect.x > board.surface.get_width():
130 self.bounce_x()
131
132 if self.rect.y < 0 or self.rect.y > board.surface.get_height():
133 self.bounce_y()
134
135 for racket in args:
136 if self.rect.colliderect(racket.rect):
137 self.bounce_y()
Tak jak w przypadku dodawania piłeczki, rakietkę też trzeba dodać do „gry”, dodatkowo musimy ją pokazać piłeczce:
38class PongGame(object):
39 """
40 Łączy wszystkie elementy gry w całość.
41 """
42
43 def __init__(self, width, height):
44 pygame.init()
45 self.board = Board(width, height)
46 # zegar którego użyjemy do kontrolowania szybkości rysowania
47 # kolejnych klatek gry
48 self.fps_clock = pygame.time.Clock()
49 self.ball = Ball(width=20, height=20, x=width/2, y=height/2)
50 self.player1 = Racket(width=80, height=20, x=width/2, y=height/2)
51
52 def run(self):
53 """
54 Główna pętla programu
55 """
56 while not self.handle_events():
57 # działaj w pętli do momentu otrzymania sygnału do wyjścia
58 self.ball.move(self.board, self.player1)
59 self.board.draw(
60 self.ball,
61 self.player1,
62 )
63 self.fps_clock.tick(30)
64
65 def handle_events(self):
66 """
67 Obsługa zdarzeń systemowych, tutaj zinterpretujemy np. ruchy myszką
68
69 :return True jeżeli pygame przekazał zdarzenie wyjścia z gry
70 """
71 for event in pygame.event.get():
72 if event.type == pygame.locals.QUIT:
73 pygame.quit()
74 return True
75
76 if event.type == pygame.locals.MOUSEMOTION:
77 # myszka steruje ruchem pierwszego gracza
78 x, y = event.pos
79 self.player1.move(x)
Gotowy kod możemy pobrać komendą:
~/python101$ git checkout -f pong/z5
Informacja
W tym miejscu można się pobawić naszą grą. Zmodyfikuj ją według uznania i podziel się rezultatem z innymi. Jeśli kod przestanie działać, można szybko cofnąć zmiany poniższą komendą.
~/python101$ git reset --hard
4.2.6. Gramy przeciwko komputerowi
Dodajemy przeciwnika, nasz przeciwnik będzie mistrzem, będzie dokładnie śledził piłeczkę i zawsze starał się utrzymać rakietkę gotową do odbicia piłeczki.
167
168class Ai(object):
169 """
170 Przeciwnik, steruje swoją rakietką na podstawie obserwacji piłeczki.
171 """
172 def __init__(self, racket, ball):
173 self.ball = ball
174 self.racket = racket
175
176 def move(self):
177 x = self.ball.rect.centerx
178 self.racket.move(x)
Tak jak w przypadku piłeczki i rakietki dodajemy nasze Ai
do gry,
a wraz nią drugą rakietkę.
Rakietki ustawiamy na przeciwległych brzegach planszy.
Trzeba pamiętać, by pokazać drugą rakietkę piłeczce, tak by mogła się od niej odbijać.
38class PongGame(object):
39 """
40 Łączy wszystkie elementy gry w całość.
41 """
42
43 def __init__(self, width, height):
44 pygame.init()
45 self.board = Board(width, height)
46 # zegar którego użyjemy do kontrolowania szybkości rysowania
47 # kolejnych klatek gry
48 self.fps_clock = pygame.time.Clock()
49 self.ball = Ball(width=20, height=20, x=width/2, y=height/2)
50 self.player1 = Racket(width=80, height=20, x=width/2 - 40, y=height - 40)
51 self.player2 = Racket(width=80, height=20, x=width/2 - 40, y=20, color=(0, 0, 0))
52 self.ai = Ai(self.player2, self.ball)
53
54 def run(self):
55 """
56 Główna pętla programu
57 """
58 while not self.handle_events():
59 # działaj w pętli do momentu otrzymania sygnału do wyjścia
60 self.ball.move(self.board, self.player1, self.player2)
61 self.board.draw(
62 self.ball,
63 self.player1,
64 self.player2,
65 )
66 self.ai.move()
67 self.fps_clock.tick(30)
4.2.7. Pokazujemy punkty
Dodajmy klasę sędziego, który patrząc na poszczególne elementy gry będzie decydował, czy graczom należą się punkty i będzie ustawiał piłkę w początkowym położeniu.
184
185
186class Judge(object):
187 """
188 Sędzia gry
189 """
190
191 def __init__(self, board, ball, *args):
192 self.ball = ball
193 self.board = board
194 self.rackets = args
195 self.score = [0, 0]
196
197 # Przed pisaniem tekstów, musimy zainicjować mechanizmy wyboru fontów PyGame
198 pygame.font.init()
199 font_path = pygame.font.match_font('arial')
200 self.font = pygame.font.Font(font_path, 64)
201
202 def update_score(self, board_height):
203 """
204 Jeśli trzeba przydziela punkty i ustawia piłeczkę w początkowym położeniu.
205 """
206 if self.ball.rect.y < 0:
207 self.score[0] += 1
208 self.ball.reset()
209 elif self.ball.rect.y > board_height:
210 self.score[1] += 1
211 self.ball.reset()
212
213 def draw_text(self, surface, text, x, y):
214 """
215 Rysuje wskazany tekst we wskazanym miejscu
216 """
217 text = self.font.render(text, True, (150, 150, 150))
218 rect = text.get_rect()
219 rect.center = x, y
220 surface.blit(text, rect)
221
222 def draw_on(self, surface):
223 """
224 Aktualizuje i rysuje wyniki
225 """
226 height = self.board.surface.get_height()
227 self.update_score(height)
228
229 width = self.board.surface.get_width()
230 self.draw_text(surface, "Player: {}".format(self.score[0]), width/2, height * 0.3)
231 self.draw_text(surface, "Computer: {}".format(self.score[1]), width/2, height * 0.7)
Jak zwykle dodajemy instancję nowej klasy do gry:
38class PongGame(object):
39 """
40 Łączy wszystkie elementy gry w całość.
41 """
42
43 def __init__(self, width, height):
44 pygame.init()
45 self.board = Board(width, height)
46 # zegar którego użyjemy do kontrolowania szybkości rysowania
47 # kolejnych klatek gry
48 self.fps_clock = pygame.time.Clock()
49 self.ball = Ball(width=20, height=20, x=width/2, y=height/2)
50 self.player1 = Racket(width=80, height=20, x=width/2 - 40, y=height - 40)
51 self.player2 = Racket(width=80, height=20, x=width/2 - 40, y=20, color=(0, 0, 0))
52 self.ai = Ai(self.player2, self.ball)
53 self.judge = Judge(self.board, self.ball, self.player2, self.ball)
54
55 def run(self):
56 """
57 Główna pętla programu
58 """
59 while not self.handle_events():
60 # działaj w pętli do momentu otrzymania sygnału do wyjścia
61 self.ball.move(self.board, self.player1, self.player2)
62 self.board.draw(
63 self.ball,
64 self.player1,
65 self.player2,
66 self.judge,
67 )
68 self.ai.move()
69 self.fps_clock.tick(30)
70
4.2.8. Zadania dodatkowe
Piłeczka „odbija się” po zewnętrznej prawej i dolnej krawędzi. Można to poprawić.
Metoda
Ball.move
otrzymuje w argumentach planszę i rakietki. Te elementy można piłeczce przekazać tylko raz w konstruktorze.Komputer nie odbija piłeczkę rogiem rakietki.
Rakietka gracza rusza się tylko, gdy gracz rusza myszką, ruch w stronę myszki powinien być kontynuowany także, gdy myszka jest bezczynna.
Gdy piłeczka odbija się od boków rakietki, powinna odbijać się w osi X.
Gra dwuosobowa z użyciem komunikacji po sieci.
Materiały Python 101
udostępniane przez
Centrum Edukacji Obywatelskiej na licencji
Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowa.
- Utworzony:
2025-04-12 o 10:21 w Sphinx 7.3.7
- Autorzy: