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 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 | # coding=utf-8
import pygame
import pygame.locals
class Board(object):
"""
Plansza do gry. Odpowiada za rysowanie okna gry.
"""
def __init__(self, width, height):
"""
Konstruktor planszy do gry. Przygotowuje okienko gry.
:param width:
:param height:
"""
self.surface = pygame.display.set_mode((width, height), 0, 32)
pygame.display.set_caption('Simple Pong')
def draw(self, *args):
"""
Rysuje okno gry
:param args: lista obiektów do narysowania
"""
background = (230, 255, 255)
self.surface.fill(background)
for drawable in args:
drawable.draw_on(self.surface)
# dopiero w tym miejscu następuje fatyczne rysowanie
# w oknie gry, wcześniej tylko ustalaliśmy co i jak ma zostać narysowane
pygame.display.update()
board = Board(800, 400)
board.draw()
|
W powyższym kodzie zdefiniowaliśmy klasę Board
z dwiema metodami:
- konstruktorem
__init__
, oraz - metodą
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:
39 40 | import time
time.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:
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 | class PongGame(object):
"""
Łączy wszystkie elementy gry w całość.
"""
def __init__(self, width, height):
pygame.init()
self.board = Board(width, height)
# zegar którego użyjemy do kontrolowania szybkości rysowania
# kolejnych klatek gry
self.fps_clock = pygame.time.Clock()
def run(self):
"""
Główna pętla programu
"""
while not self.handle_events():
# działaj w pętli do momentu otrzymania sygnału do wyjścia
self.board.draw()
self.fps_clock.tick(30)
def handle_events(self):
"""
Obsługa zdarzeń systemowych, tutaj zinterpretujemy np. ruchy myszką
:return True jeżeli pygame przekazał zdarzenie wyjścia z gry
"""
for event in pygame.event.get():
if event.type == pygame.locals.QUIT:
pygame.quit()
return True
# Ta część powinna być zawsze na końcu modułu (ten plik jest modułem)
# chcemy uruchomić naszą grę dopiero po tym jak wszystkie klasy zostaną zadeklarowane
if __name__ == "__main__":
game = PongGame(800, 400)
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:
71 72 73 74 75 76 77 78 79 80 81 82 83 84 | class Drawable(object):
"""
Klasa bazowa dla rysowanych obiektów
"""
def __init__(self, width, height, x, y, color=(0, 255, 0)):
self.width = width
self.height = height
self.color = color
self.surface = pygame.Surface([width, height], pygame.SRCALPHA, 32).convert_alpha()
self.rect = self.surface.get_rect(x=x, y=y)
def draw_on(self, surface):
surface.blit(self.surface, self.rect)
|
Następnie dodajmy klasę samej piłeczki dziedzicząc z Drawable
:
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 | class Ball(Drawable):
"""
Piłeczka, sama kontroluje swoją prędkość i kierunek poruszania się.
"""
def __init__(self, width, height, x, y, color=(255, 0, 0), x_speed=3, y_speed=3):
super(Ball, self).__init__(width, height, x, y, color)
pygame.draw.ellipse(self.surface, self.color, [0, 0, self.width, self.height])
self.x_speed = x_speed
self.y_speed = y_speed
self.start_x = x
self.start_y = y
def bounce_y(self):
"""
Odwraca wektor prędkości w osi Y
"""
self.y_speed *= -1
def bounce_x(self):
"""
Odwraca wektor prędkości w osi X
"""
self.x_speed *= -1
def reset(self):
"""
Ustawia piłeczkę w położeniu początkowym i odwraca wektor prędkości w osi Y
"""
self.rect.move(self.start_x, self.start_y)
self.bounce_y()
def move(self):
"""
Przesuwa piłeczkę o wektor prędkości
"""
self.rect.x += self.x_speed
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:
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 | class PongGame(object):
"""
Łączy wszystkie elementy gry w całość.
"""
def __init__(self, width, height):
pygame.init()
self.board = Board(width, height)
# zegar którego użyjemy do kontrolowania szybkości rysowania
# kolejnych klatek gry
self.fps_clock = pygame.time.Clock()
self.ball = Ball(20, 20, width/2, height/2)
def run(self):
"""
Główna pętla programu
"""
while not self.handle_events():
# działaj w pętli do momentu otrzymania sygnału do wyjścia
self.ball.move()
self.board.draw(
self.ball,
)
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ć:
122 123 124 125 126 127 128 129 130 131 132 133 | def move(self, board):
"""
Przesuwa piłeczkę o wektor prędkości
"""
self.rect.x += self.x_speed
self.rect.y += self.y_speed
if self.rect.x < 0 or self.rect.x > board.surface.get_width():
self.bounce_x()
if self.rect.y < 0 or self.rect.y > board.surface.get_height():
self.bounce_y()
|
Jeszcze zmodyfikujmy wywołanie metody move
w naszej pętli głównej:
51 52 53 54 55 56 57 58 59 60 | def run(self):
"""
Główna pętla programu
"""
while not self.handle_events():
self.ball.move(self.board)
self.board.draw(
self.ball,
)
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.
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | class Racket(Drawable):
"""
Rakietka, porusza się w osi X z ograniczeniem prędkości.
"""
def __init__(self, width, height, x, y, color=(0, 255, 0), max_speed=10):
super(Racket, self).__init__(width, height, x, y, color)
self.max_speed = max_speed
self.surface.fill(color)
def move(self, x):
"""
Przesuwa rakietkę w wyznaczone miejsce.
"""
delta = x - self.rect.x
if abs(delta) > self.max_speed:
delta = self.max_speed if delta > 0 else -self.max_speed
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:
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | def move(self, board, *args):
"""
Przesuwa piłeczkę o wektor prędkości
"""
self.rect.x += self.x_speed
self.rect.y += self.y_speed
if self.rect.x < 0 or self.rect.x > board.surface.get_width():
self.bounce_x()
if self.rect.y < 0 or self.rect.y > board.surface.get_height():
self.bounce_y()
for racket in args:
if self.rect.colliderect(racket.rect):
self.bounce_y()
|
Tak jak w przypadku dodawania piłeczki, rakietkę też trzeba dodać do “gry”, dodatkowo musimy ją pokazać piłeczce:
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 | class PongGame(object):
"""
Łączy wszystkie elementy gry w całość.
"""
def __init__(self, width, height):
pygame.init()
self.board = Board(width, height)
# zegar którego użyjemy do kontrolowania szybkości rysowania
# kolejnych klatek gry
self.fps_clock = pygame.time.Clock()
self.ball = Ball(width=20, height=20, x=width/2, y=height/2)
self.player1 = Racket(width=80, height=20, x=width/2, y=height/2)
def run(self):
"""
Główna pętla programu
"""
while not self.handle_events():
# działaj w pętli do momentu otrzymania sygnału do wyjścia
self.ball.move(self.board, self.player1)
self.board.draw(
self.ball,
self.player1,
)
self.fps_clock.tick(30)
def handle_events(self):
"""
Obsługa zdarzeń systemowych, tutaj zinterpretujemy np. ruchy myszką
:return True jeżeli pygame przekazał zdarzenie wyjścia z gry
"""
for event in pygame.event.get():
if event.type == pygame.locals.QUIT:
pygame.quit()
return True
if event.type == pygame.locals.MOUSEMOTION:
# myszka steruje ruchem pierwszego gracza
x, y = event.pos
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 168 169 170 171 172 173 174 175 176 177 178 |
class Ai(object):
"""
Przeciwnik, steruje swoją rakietką na podstawie obserwacji piłeczki.
"""
def __init__(self, racket, ball):
self.ball = ball
self.racket = racket
def move(self):
x = self.ball.rect.centerx
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ć.
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 | class PongGame(object):
"""
Łączy wszystkie elementy gry w całość.
"""
def __init__(self, width, height):
pygame.init()
self.board = Board(width, height)
# zegar którego użyjemy do kontrolowania szybkości rysowania
# kolejnych klatek gry
self.fps_clock = pygame.time.Clock()
self.ball = Ball(width=20, height=20, x=width/2, y=height/2)
self.player1 = Racket(width=80, height=20, x=width/2 - 40, y=height - 40)
self.player2 = Racket(width=80, height=20, x=width/2 - 40, y=20, color=(0, 0, 0))
self.ai = Ai(self.player2, self.ball)
def run(self):
"""
Główna pętla programu
"""
while not self.handle_events():
# działaj w pętli do momentu otrzymania sygnału do wyjścia
self.ball.move(self.board, self.player1, self.player2)
self.board.draw(
self.ball,
self.player1,
self.player2,
)
self.ai.move()
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 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 |
class Judge(object):
"""
Sędzia gry
"""
def __init__(self, board, ball, *args):
self.ball = ball
self.board = board
self.rackets = args
self.score = [0, 0]
# Przed pisaniem tekstów, musimy zainicjować mechanizmy wyboru fontów PyGame
pygame.font.init()
font_path = pygame.font.match_font('arial')
self.font = pygame.font.Font(font_path, 64)
def update_score(self, board_height):
"""
Jeśli trzeba przydziela punkty i ustawia piłeczkę w początkowym położeniu.
"""
if self.ball.rect.y < 0:
self.score[0] += 1
self.ball.reset()
elif self.ball.rect.y > board_height:
self.score[1] += 1
self.ball.reset()
def draw_text(self, surface, text, x, y):
"""
Rysuje wskazany tekst we wskazanym miejscu
"""
text = self.font.render(text, True, (150, 150, 150))
rect = text.get_rect()
rect.center = x, y
surface.blit(text, rect)
def draw_on(self, surface):
"""
Aktualizuje i rysuje wyniki
"""
height = self.board.surface.get_height()
self.update_score(height)
width = self.board.surface.get_width()
self.draw_text(surface, "Player: {}".format(self.score[0]), width/2, height * 0.3)
self.draw_text(surface, "Computer: {}".format(self.score[1]), width/2, height * 0.7)
|
Jak zwykle dodajemy instancję nowej klasy do gry:
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 | class PongGame(object):
"""
Łączy wszystkie elementy gry w całość.
"""
def __init__(self, width, height):
pygame.init()
self.board = Board(width, height)
# zegar którego użyjemy do kontrolowania szybkości rysowania
# kolejnych klatek gry
self.fps_clock = pygame.time.Clock()
self.ball = Ball(width=20, height=20, x=width/2, y=height/2)
self.player1 = Racket(width=80, height=20, x=width/2 - 40, y=height - 40)
self.player2 = Racket(width=80, height=20, x=width/2 - 40, y=20, color=(0, 0, 0))
self.ai = Ai(self.player2, self.ball)
self.judge = Judge(self.board, self.ball, self.player2, self.ball)
def run(self):
"""
Główna pętla programu
"""
while not self.handle_events():
# działaj w pętli do momentu otrzymania sygnału do wyjścia
self.ball.move(self.board, self.player1, self.player2)
self.board.draw(
self.ball,
self.player1,
self.player2,
self.judge,
)
self.ai.move()
self.fps_clock.tick(30)
|
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: | 2022-05-22 o 19:52 w Sphinx 1.5.3 |
---|---|
Autorzy: | Patrz plik “Autorzy” |