44 lekcje — od pustego okna do pełnej gry z rekordem zapisywanym na dysk. Każda lekcja to działający program.
Witaj w Snake XTRA. W tym kursie zbudujesz klasyczną grę Snake — ale nie pobierzesz gotowca ani nie przepiszesz cudzego szablonu. Napiszesz własny kod, linia po linii, rozumiejąc każdą z nich. Zaczniesz od pustego okna, a skończysz na kompletnej grze: wąż rośnie z każdym zjedzonym jabłkiem, przyspiesza na kolejnych poziomach, pojawia się ekran Game Over, a rekord zapisany na dysku przetrwa zamknięcie programu. A pętla gry, obsługa klawiszy i zapisywanie stanu, które tu poznasz, to fundament każdej interaktywnej aplikacji — nie tylko gier.
Zanim ruszysz, warto wiedzieć, jak zbudowany jest ten kurs — bo jego rytm jest celowy i będzie Ci towarzyszył przez wszystkie 44 kroki. To materiał na kilkanaście godzin spokojnej nauki — lekcja lub dwie dziennie to świetny rytm; liczy się regularność, nie tempo.
Kurs przeplata dwa rodzaje lekcji. Lekcje oznaczone literą T to teoria — każda bierze jedno pojęcie Pythona lub Pygame (krotkę, pętlę zdarzeń, kolizję) i rozkłada je na części: metafora, kod, eksperyment do samodzielnego sprawdzenia, zadania. Lekcje oznaczone literą S to Snake — w nich nie poznajesz nowej teorii, tylko składasz zdobyte narzędzia w działający fragment gry. Zasada jest prosta: T daje Ci narzędzie do ręki, S pokazuje, jak zbudować nim kawałek gry. Najpierw rozumiesz, potem budujesz.
Przez cały kurs pracujesz w jednym pliku — snake.py. Zaczyna się on od kilkunastu linii (samo okno), a kończy na ponad setce (pełna gra). Nie zaczynasz od zera przy każdej lekcji: każda lekcja S dokłada nowe linie do tego, co już masz. Twój plik rośnie razem z grą — i pod koniec trzymasz w ręku grę, której każdy fragment napisałeś samodzielnie. W lekcjach S będziemy często prosić, żebyś pisał z pamięci, a dopiero potem porównywał kod ze wzorcem — bo między „rozumiem, gdy czytam” a „umiem napisać sam” jest przepaść, którą zasypuje tylko praktyka.
for i while) oraz funkcjami, bo Snake opiera się na nich od pierwszych lekcji. Jeśli któreś z tych pojęć jest mgliste — wróć najpierw do podstaw, a tutaj poradzisz sobie znacznie łatwiej.Zanim napiszesz pierwszą linię gry, potrzebujesz Pythona 3.12 (silnika, który uruchamia Twój kod), Pygame (biblioteki do grafiki i gier — więcej w T01) oraz edytora VS Code. Przygotujesz to raz — naszą instrukcją krok po kroku, ze zrzutem ekranu do każdego kliknięcia.
python --version
python -m pip install pygame
python3 zamiast python: najpierw python3 -m pip install pygame, potem python3 snake.py.sudo apt install python3-pip, potem python3 -m pip install pygame i python3 snake.py.python, u Ciebie to python3. Cała reszta — kod, klawisze, pliki — jest identyczna.
'python' is not recognized → przejdź instrukcję devostro.pl/start od początku i pamiętaj o zaznaczeniu „Add python.exe to PATH”. ModuleNotFoundError: No module named 'pygame' → VS Code odpala inny Python niż ten z Pygame: Ctrl+Shift+P → „Python: Select Interpreter” → wybierz tego, w którym instalowałeś Pygame.
snake) stwórz pusty plik snake.py — to w nim, krok po kroku, powstanie cała gra. Nie usuwaj go po lekcji; napiszemy go razem w T01.python --version pokazuje 3.12.xpython -m pip install pygame kończy się „Successfully installed pygame-2.6.1”snake.py w folderze projektusnake.py czeka na pierwszą linię. Od tego momentu zaczynasz pisać własny kod: w T01 najpierw zobaczysz na żywo, czemu program bez jednego kluczowego elementu się wiesza, a potem sam napiszesz wersję, która działa — rozumiejąc każdą linię. Czas zacząć budować.
import pygame to wejście do studia: mówisz Pythonowi, że potrzebujesz zestawu narzędzi do budowania gier. pygame.init() to włączenie zasilania — uruchomienie grafiki, dźwięku i klawiatury. Dopiero po tych dwóch krokach możesz cokolwiek zrobić. Tak zaczyna się każdy program Pygame i każda gra, którą kiedykolwiek napiszesz.
Zanim wpiszesz pierwszą linię, warto zrozumieć, czym jest to, co za chwilę zaimportujesz. Pygame to biblioteka — gotowy zestaw funkcji napisany przez innych programistów. Jej twórcy spędzili lata, rozwiązując problemy, które nie mają nic wspólnego z mechaniką Twojej gry: jak otworzyć okno na Windows, Mac i Linux jednocześnie, jak zsynchronizować rysowanie z kartą graficzną, żeby ekran nie migotał, jak odczytywać klawiaturę bez opóźnień. Znasz ten mechanizm z kursu podstaw — import random dawał Ci od razu random.randint(), bez pisania własnego generatora liczb. Tutaj jedna linia importu daje Ci okna, rysowanie, klawiaturę i dźwięk. Ty zajmujesz się tym, co najciekawsze: budowaniem gry.
Zwróć jeszcze uwagę na kropkę w zapisach takich jak pygame.display.set_mode(). To ścieżka do funkcji, czytana od lewej: z pakietu pygame, z modułu display, wywołaj funkcję set_mode. Każdy taki zapis w tym kursie czytasz tak samo: skąd pochodzi, z którego modułu, co robi.
Każda gra zbudowana na Pygame — od najprostszej do najbardziej złożonej — korzysta z tych samych kroków w tej samej kolejności. To nie konwencja, tylko konsekwencja tego, jak działa grafika i system operacyjny. Gdy ten szkielet zapamiętasz, będziesz pisał własne gry bez zaglądania do kursu.
Najpierw pygame.init() budzi wszystkie podsystemy biblioteki — grafikę, dźwięk, obsługę wejścia. Potem pygame.display.set_mode() tworzy okno: podajesz wymiary w pikselach — w Snake kwadrat 600×600 — i dostajesz obiekt będący powierzchnią do rysowania. Ten obiekt, który nazwiesz ekran, będzie Ci towarzyszył przez cały kurs: każde ekran.fill() i każde pygame.draw.rect() trafi właśnie do niego. Przed pętlą główną definiujesz jeszcze stałe — nazwane wartości dla wymiarów, kolorów i prędkości; po co — zobaczysz za chwilę na własnym kodzie.
I wreszcie serce programu — pętla while True, która kręci się bez przerwy dopóki gra działa. W każdym obiegu trzy rzeczy dzieją się po kolei: sprawdź, co się stało od ostatniej klatki, oblicz nowy stan gry, narysuj go na ekranie. Tę trójfazową strukturę poznasz dokładnie w T02. Na razie zapamiętaj jedno: wszystko, co dojdzie do kodu w następnych lekcjach — ruch węża, jabłko, wynik — wejdzie dokładnie w jedno z tych trzech miejsc. Struktura się nie zmienia, zmienia się tylko zawartość.
snake.py — pusty plik z lekcji Setup. Cały kod tego kursu piszesz w tym jednym pliku. Nie tworzysz nowych plików, nie rozdzielasz na moduły. Jeden plik rośnie razem z grą, od początku do końca.
Najłatwiej zapamiętać, dlaczego coś jest potrzebne, gdy zobaczysz, co się dzieje bez tego. Dlatego zaczniesz od wersji celowo niepełnej — brakuje w niej jednego ważnego elementu. Najpierw zobaczysz efekt braku, potem napiszesz wersję kompletną, już rozumiejąc każdą linię. Wklej poniższy kod do snake.py, zapisz (Ctrl+S) i uruchom:
import pygame
pygame.init()
ekran = pygame.display.set_mode((600, 600))
while True:
ekran.fill((15, 25, 15))
pygame.display.flip()
Okno się pojawiło — ale teraz kliknij X. Nic się nie dzieje. Po chwili okno może dostać oznaczenie „(Not Responding)”. Jedyne wyjście to Ctrl+C w terminalu.
Co tu się dzieje? Program kręci się w pętli: zamaluj ekran, wyświetl, zamaluj, wyświetl — setki razy na sekundę. W każdym obiegu powinien zapytać system: czy coś się zdarzyło? Ktoś kliknął X? W tym kodzie tego pytania nie ma. System wysyła komunikat o kliknięciu, program go nie odbiera, komunikaty piętrzą się w kolejce — i po chwili system uznaje program za zawieszony. Zapamiętaj to doświadczenie: ten jeden brakujący element to najczęstsza przyczyna zamrożonych okien u początkujących.
pygame window. Za chwilę ustawisz własny przez pygame.display.set_caption("SNAKE") — napis pojawi się na pasku tytułowym okna.Czas naprawić program — tym razem rozumiejąc każdą nową linię. Do kodu wejdą trzy elementy.
Pierwszy to nazwane stałe zamiast gołych liczb. W poprzedniej wersji masz set_mode((600, 600)) — dwie liczby bez nazwy. Za miesiąc nie będziesz pamiętał, co oznacza pierwsze 600, a co drugie. Programiści nazywają takie liczby magic numbers: mają sens w chwili pisania, potem stają się zagadką. Rozwiązanie — dajesz wartości nazwę: SZEROKOSC = 600. Od tej chwili wszędzie używasz nazwy, a liczba siedzi w jednym miejscu na górze pliku. Zmiana rozmiaru okna to edycja jednego wiersza, nie szukanie po całym pliku.
Drugi to obsługa zamknięcia okna. pygame.event.get() zwraca listę zdarzeń, które zaszły od ostatniej klatki — kliknięcia, klawisze, zamknięcie okna. Przeglądasz ją pętlą for i reagujesz na to, co Cię interesuje. Kliknięcie X generuje zdarzenie pygame.QUIT — wtedy wykonujesz dwa kroki: pygame.quit() zwalnia zasoby Pygame, a raise SystemExit kończy program Pythona. Dwie linie, bo to dwie osobne czynności: sprzątanie po bibliotece i wyjście z programu.
Trzeci to tytuł okna — jedna linia: pygame.display.set_caption("SNAKE"). Zastąp cały plik tym kodem i uruchom:
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
KOLOR_TLO = (15, 25, 15)
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE")
while True:
for event in pygame.event.get(): # opróżniaj kolejkę zdarzeń każdą klatkę
if event.type == pygame.QUIT:
pygame.quit() # zwolnij zasoby Pygame
raise SystemExit # zakończ program
ekran.fill(KOLOR_TLO) # wyczyść ekran (nowa klatka)
pygame.display.flip() # wyślij gotową klatkę na monitor
Działa. Tytuł na pasku, kliknięcie X kończy program porządnie. Zwróć uwagę na strukturę: wszystko przed while True to inicjalizacja — wykonuje się raz przy starcie. Wewnątrz pętli każda klatka robi to samo: odbiera zdarzenia, czyści ekran, wyświetla. Ta architektura zostaje do końca kursu — dodasz dziesiątki linii, ale każda trafi w jedno z tych miejsc. Szkielet jest gotowy, teraz będziesz go wypełniał.
Jednej rzeczy w tym kodzie jeszcze nie ma: kontroli czasu. Pętla kręci się tak szybko, jak pozwala procesor — tysiąc klatek na sekundę na szybkim komputerze, sto na wolnym. W T03 dodasz zegar.tick(8), który ustali równe 8 klatek na sekundę — bo wąż ma się poruszać w kontrolowanym tempie, nie „ile zdąży”. Na razie to nie przeszkadza: okno jest puste i nie ma co animować.
To umowa programistów, nie reguła języka. Nazwy WIELKIMI_LITERAMI oznaczają stałe — wartości, które definiujesz raz i nie zmieniasz podczas działania programu. Python nie zabroni Ci napisać SZEROKOSC = 800 w środku kodu, ale każdy czytający — łącznie z Tobą za kilka tygodni — od razu zobaczy, że coś jest nie tak. Wielkie litery to sygnał: „tej wartości nie ruszaj, ona jest fundamentem”. W Snake będziesz miał stałe dla rozmiaru okna, rozmiaru kratki, kolorów i prędkości — wszystkie WIELKIMI, wszystkie na górze pliku, zanim cokolwiek innego.
Zostało jedno pojęcie: RGB, czyli język kolorów w Pygame. Każdy piksel monitora świeci trzema diodami — czerwoną (Red), zieloną (Green) i niebieską (Blue) — a każda z nich ma jasność od 0 (wyłączona) do 255 (maksimum). Trzy liczby opisują dowolny kolor: (0, 0, 0) to czarny — brak światła. (255, 255, 255) to biały — wszystko na maksimum. (255, 0, 0) to czysty czerwony — świeci tylko jedna dioda. A tło planszy Snake, (15, 25, 15), to bardzo ciemna zieleń — odrobina każdego kanału z przewagą zielonego.
Trzy liczby w nawiasach okrągłych to w Pythonie krotka — niezmienny zestaw wartości. O krotkach dokładnie w T10, ale już teraz widzisz je przy każdym kolorze i rozmiarze okna. Zwróć uwagę na podwójny nawias w set_mode((600, 600)): zewnętrzny to wywołanie funkcji, wewnętrzny to krotka będąca argumentem. Ten wzorzec jeszcze wróci.
Kolory, których użyjesz w Snake — warto je znać od początku:
(15, 25, 15) — ciemnozielony — tło planszy(80, 200, 80) — zielony — ciało węża(220, 60, 60) — czerwony — jabłko(40, 120, 40) — ciemnozielony — linie siatki(255, 255, 255) — biały — tekst i UISpędź 3–4 minuty na eksperymentowaniu. Zmień wartość KOLOR_TLO, zapisz (Ctrl+S) i uruchom. Światło miesza się inaczej niż farby — a to doświadczenie zostanie w głowie dłużej niż czytanie o nim.
(200, 0, 0) — tylko czerwony kanał aktywny. Teraz (0, 0, 200) — tylko niebieski. Jaka różnica w wyglądzie?(200, 200, 0) — czerwony i zielony razem, bez niebieskiego. Jakiego koloru się spodziewałeś? Jakiego dostałeś?(80, 80, 80) i (180, 180, 180) — równe wartości we wszystkich kanałach. Co dostaniesz?(200, 0, 0) = ciemnoczerwone, (0, 0, 200) = ciemnoniebieskie.
(200, 200, 0) = żółty — czerwony + zielony = żółty. To nieintuicyjne dla kogoś kto mieszał farby, ale światło miesza się inaczej niż pigmenty.
Równe wartości we wszystkich kanałach = odcień szarości. Małe liczby = ciemna szarość, duże = jasna. (128, 128, 128) = neutralna szarość środkowa.
for event in pygame.event.get(): wraz z obsługą QUIT. Uruchom i spróbuj zamknąć okno. Obserwuj co się dzieje i kiedy. Przywróć kod do działającego stanu.KOLOR_TLO na kolor, który znalazłeś w eksperymencie. Uruchom i sprawdź efekt.pygame.quit() i raise SystemExit: co robi każda z nich i czym się różnią? To ćwiczenie zrozumienia, nie wpisywania.import pygame i pygame.init() oraz wyjaśnić, co każde robi i dlaczego kolejność ma znaczeniepygame.quit() i raise SystemExit — i wyjaśnić, dlaczego to dwa oddzielne kroki
W T01 napisałeś już działający game loop — pętlę while True, która odbiera zdarzenia i odświeża ekran. Działa, ale wygląda jak jeden ciąg instrukcji bez wyraźnych granic. Teraz nadasz jej strukturę, bo każdy game loop na świecie — od Snake po najnowsze gry AAA — składa się z trzech faz wykonywanych co klatkę, zawsze w tej samej kolejności.
Najpierw ZDARZENIA: pytasz system, co się stało od ostatniej klatki — czy gracz nacisnął klawisz, kliknął myszą, zamknął okno. Potem LOGIKA: na podstawie tego, co się stało, obliczasz nowy stan gry — przesuwasz węża o kratkę, sprawdzasz, czy trafił w jabłko, aktualizujesz wynik. Na końcu RYSOWANIE: bierzesz świeżo obliczony stan i malujesz go na ekranie.
Ta kolejność nie jest umowna — wynika z logiki przyczyny i skutku. Gdybyś rysował przed logiką, pokazywałbyś graczowi stan z poprzedniej klatki: wąż zawsze „krok za” tym, gdzie naprawdę jest. Gdybyś zbierał zdarzenia po logice, naciśnięcie strzałki zadziałałoby dopiero w następnej klatce — przy 8 klatkach na sekundę to odczuwalne ósme części sekundy opóźnienia na każdy ruch. Zdarzenia, logika, rysowanie. Zawsze tak.
snake.py z kodem, który skończyłeś w T01 — stałe, okno z tytułem, pętla z obsługą QUIT. Niczego nie kasujesz: w tej lekcji nadasz temu kodowi strukturę i dodasz obsługę klawiatury.
Pierwszy krok jest prosty, ale ważniejszy niż wygląda: dodasz do pętli trzy komentarze, które wyznaczą strefy faz. Nie zmieniasz żadnej logiki — porządkujesz. Za kilka lekcji w strefie LOGIKA będzie dwadzieścia linii, w RYSOWANIE piętnaście, i bez tych granic wszystko zlałoby się w jedną masę, w której nie wiadomo, gdzie co wstawić. Zastąp swoją pętlę tą wersją:
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
KOLOR_TLO = (15, 25, 15)
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE")
while True:
# ─── 1. ZDARZENIA ─────────────────────────────────
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── 2. LOGIKA ────────────────────────────────────
# (na razie pusta — gra nic jeszcze nie oblicza)
# ─── 3. RYSOWANIE ─────────────────────────────────
ekran.fill(KOLOR_TLO)
pygame.display.flip()
Okno wygląda tak samo jak przed chwilą — i o to chodzi. Zmieniła się architektura, nie zachowanie. Od teraz, gdy w kolejnych lekcjach przeczytasz „dodaj to do sekcji LOGIKA”, będziesz dokładnie wiedział, gdzie patrzeć. Pamiętasz z T01, co się działo, gdy w pętli zabrakło event.get() — okno zamarzało, bo kolejka zdarzeń systemu się przepełniała. Teraz widzisz to samo z lotu ptaka: faza ZDARZENIA jest jedyną, której nigdy nie wolno pominąć, nawet gdy LOGIKA jest pusta.
Do tej pory Twoja gra reaguje tylko na jedno zdarzenie: zamknięcie okna. Czas nauczyć ją słuchać klawiatury — bo przecież wąż będzie sterowany strzałkami. Gdy gracz naciska dowolny klawisz, Pygame generuje zdarzenie typu pygame.KEYDOWN. Samo KEYDOWN mówi tylko „jakiś klawisz poszedł w dół” — który konkretnie, sprawdzasz w polu event.key. Pygame ma stałą dla każdego klawisza: K_ESCAPE to Escape, K_UP strzałka w górę, K_r litera R.
Zaczniesz od Escape jako drugiego sposobu zamykania gry — to dobra praktyka w każdej grze i najprostszy możliwy trening obsługi klawiatury, zanim w T07 te same mechanizmy posłużą do sterowania wężem. Rozbuduj sekcję ZDARZENIA do tej postaci:
# ─── 1. ZDARZENIA ─────────────────────────────────
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: # Escape zamyka grę
pygame.quit()
raise SystemExit
Uruchom i naciśnij Escape — okno znika, dokładnie jak po kliknięciu X. Zwróć uwagę na zagnieżdżenie: najpierw sprawdzasz typ zdarzenia (KEYDOWN — coś naciśnięto), dopiero wewnątrz który klawisz (K_ESCAPE). Ten dwupoziomowy wzorzec wróci w T07 przy strzałkach: jeden warunek KEYDOWN, a w nim cztery porównania event.key — po jednym na kierunek.
AttributeError: 'Event' object has no attribute 'key', często od samego ruszenia myszą? Sprawdzasz event.key bez wcześniejszego event.type == pygame.KEYDOWN — oba ify wylądowały na tym samym poziomie wcięcia. Przez kolejkę płyną też zdarzenia bez pola key (ruch myszy, QUIT) i pierwsze z nich wywraca program. Pilnuj zagnieżdżenia: najpierw typ zdarzenia, dopiero WEWNĄTRZ niego — który klawisz.
K_ESCAPE — EscapeK_RIGHT, K_LEFT, K_UP, K_DOWN — strzałkiK_r, K_p, K_SPACE — litery i spacjaNazwę dowolnego naciśniętego klawisza wypisze Ci pygame.key.name(event.key).
raise SystemExit a nie exit()?exit() też działa, ale jest przeznaczony dla interaktywnych konsol. raise SystemExit to czystsze wyjście — gwarantuje, że interpreter Pythona poprawnie kończy. Zawsze poprzedź je pygame.quit(), które zwalnia pamięć karty graficznej.
Twoja gra obsługuje dwa typy zdarzeń — ale ile ich naprawdę przepływa przez kolejkę? Zanim sprawdzisz, postaw hipotezę: jakie zdarzenia generuje samo używanie okna — klawisze, mysz, coś jeszcze? Dodaj tymczasowo print(event) do pętli zdarzeń:
for event in pygame.event.get():
if event.type != pygame.MOUSEMOTION: # pomijamy ruch myszy — za dużo linii
print(event)
if event.type == pygame.QUIT:
pygame.quit(); raise SystemExit
Uruchom. Naciskaj klawisze, klikaj, przewiń kółkiem — obserwuj terminal. Po eksperymencie usuń print.
KeyDown i KeyUp — naciśnięcie i puszczenie to dwa osobne zdarzenia — oraz MouseButtonDown i MouseWheel. Każde zdarzenie niesie dodatkowe dane: kod klawisza, pozycję kursora, modyfikatory. Pygame rejestruje wszystko — a Twój kod wybiera z tego strumienia tylko to, na co chce reagować. W Snake: QUIT i KEYDOWN, resztę ignorujesz.Q (stała pygame.K_q) — obok Escape. Przetestuj oba.while True). Uruchom — działa? Dlaczego w pustym oknie nie widać różnicy i kiedy ten błąd stałby się widoczny? Przywróć poprawną kolejność.event.type a event.keyClock i FPS — metronom, który sprawi, że Snake będzie działał dokładnie tak samo na każdym sprzęcie.pygame.time.Clock() to metronom Twojej gry. Wywołanie zegar.tick(8) mówi: „poczekaj tyle, żeby cała klatka trwała dokładnie jedną ósmą sekundy”. Bez metronomu pętla biega ile zdąży — na każdym komputerze inaczej. Z metronomem gra tyka zawsze w tym samym tempie, wszędzie.
T02 skończył się obietnicą: rozwiążemy problem pętli, która kręci się tak szybko, jak pozwala procesor. Czas zrozumieć, dlaczego to w ogóle jest problem. Wyobraź sobie, że wąż przesuwa się o jedną kratkę w każdym obiegu pętli. Na szybkim komputerze pętla wykonuje się tysiąc razy na sekundę — wąż przelatuje przez całą planszę w mgnieniu oka, gra jest nie do ogrania. Na słabym laptopie te same instrukcje wykonują się sto razy na sekundę — wąż pełza dziesięć razy wolniej. Ten sam kod, dwie zupełnie różne gry. Tak być nie może: prędkość gry musi zależeć od Ciebie, nie od sprzętu gracza.
Rozwiązaniem jest miara FPS — frames per second, czyli klatki na sekundę. To liczba mówiąca, ile pełnych obiegów pętli (a więc ile odrysowań ekranu) wykonuje się w ciągu sekundy. Ty ją ustalasz, a zegar Pygame pilnuje, żeby była dotrzymana. Dla Snake wybierzemy FPS = 8 — wąż porusza się skokowo, kratka po kratce, więc 8 ruchów na sekundę daje graczowi około 125 milisekund na reakcję przed kolejnym skokiem. Wystarczająco szybko, żeby trzymać napięcie; wystarczająco wolno, żeby dało się grać.
Mechanizm składa się z dwóch części. Przed pętlą tworzysz zegar — raz: zegar = pygame.time.Clock(). A na samym końcu każdej klatki wywołujesz zegar.tick(FPS) — i tu jest sedno: tick mierzy, ile czasu zajęła bieżąca klatka, i czeka dokładnie tyle, żeby cała trwała 1/FPS sekundy. Szybki komputer poczeka dłużej, wolny krócej — efekt końcowy identyczny na obu.
snake.py z trójfazową pętlą i obsługą Escape z T02. Dodasz trzy linie: stałą FPS = 8 do bloku stałych, utworzenie zegara przed pętlą i zegar.tick(FPS) na końcu pętli.
Zaktualizuj swój kod do tej postaci — trzy nowe linie są oznaczone komentarzami:
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
FPS = 8 # klatki na sekundę — prędkość gry
KOLOR_TLO = (15, 25, 15)
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE")
zegar = pygame.time.Clock() # tworzymy metronom
while True:
# ─── ZDARZENIA ────────────────────────────────────
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ───────────────────────────────────────
# ─── RYSOWANIE ────────────────────────────────────
ekran.fill(KOLOR_TLO) # zamaluj cały ekran
pygame.display.flip() # pokaż gotową klatkę
zegar.tick(FPS) # czekaj do końca klatki
Okno wygląda identycznie jak po T02 — i znów o to chodzi. Różnica jest niewidoczna, ale fundamentalna: procesor nie pracuje już na pełnych obrotach renderując tysiące zbędnych klatek, a tempo gry jest w Twoich rękach. Gdy w T06 wąż zacznie się ruszać, jedna zmiana stałej FPS będzie zmieniała prędkość całej gry. Tę właściwość wykorzystasz zresztą na końcu kursu — w finalnej wersji Snake FPS będzie rósł z każdym zjedzonym jabłkiem, podnosząc poziom trudności.
zegar.tick(FPS) — na początku czy na końcu klatki?display.flip(). To standardowe miejsce: tick() mierzy czas od swojego poprzedniego wywołania i dośpia tak, żeby cała klatka (zdarzenia → logika → rysowanie) trwała 1/FPS sekundy. Wołamy go, gdy klatka jest już gotowa.Zostały dwie linie, które piszesz od T01, ale których jeszcze dokładnie nie omówiliśmy — a w tej lekcji domykamy całą pętlę, więc czas to nadrobić. Chodzi o parę ekran.fill() i pygame.display.flip().
ekran.fill(kolor) zamalowuje cały ekran jednym kolorem. Po co, skoro za chwilę i tak wszystko rysujesz? Bo rysowanie w Pygame nie zna pojęcia „przesuń obiekt” — każda klatka maluje wszystko od zera. Jeśli nie wyczyścisz ekranu, nowa klatka namaluje się na poprzedniej: wąż na pozycji nowej i jednocześnie wszystkie jego stare pozycje. Programiści nazywają to ghost effect — ekran zapełnia się „duchami” poprzednich klatek. Dlatego fill() zawsze otwiera fazę RYSOWANIA: czysta kartka, dopiero potem obiekty.
pygame.display.flip() rozwiązuje inny problem. Wszystko, co rysujesz — fill(), prostokąty, tekst — trafia najpierw do niewidocznego bufora w pamięci, nie prosto na monitor. Gdyby gracz widział rysowanie na żywo, ekran migotałby niedokończonymi klatkami: samo tło, tło z połową węża, wreszcie całość. flip() czeka, aż klatka jest kompletna, i dopiero wtedy pokazuje ją na monitorze — jednym ruchem, jak podniesienie kurtyny. Bez fill() masz duchy; bez flip() nie zobaczysz nigdy nic.
ekran.fill(KOLOR_TLO) i uruchom — na razie okno wygląda tak samo, bo nic się jeszcze nie rusza. Ale zapamiętaj objaw na przyszłość: gdy w T06 wąż zacznie się poruszać i zobaczysz na planszy smugę zamiast węża — to znak, że zabrakło fill(). Każda klatka rysuje się wtedy na poprzedniej i stare pozycje nigdy nie znikają. Przywróć linię, zanim pójdziesz dalej.
Twoja pętla jest kompletna: zdarzenia, logika, rysowanie, kontrola czasu. Spójrz na nią jeszcze raz w formie diagramu — od startu programu do zamknięcia. Każdy element już znasz; teraz zobaczysz je wszystkie naraz, w jednym przepływie:
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
FPS = 8
KOLOR_TLO = (15, 25, 15)
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE")
zegar = pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
ekran.fill(KOLOR_TLO)
pygame.display.flip()
zegar.tick(FPS)
Snake to gra skokowa — wąż przesuwa się o jeden segment naraz. Przy 60 FPS wąż przesuwałby się 60 razy na sekundę — za szybko na reakcję. 8 FPS to standard dla klasycznego snake: masz ~125ms na naciśnięcie strzałki przed następnym ruchem. W T06 zobaczysz, jak zmiana FPS wpływa na odczucie gry.
Zegar potrafi nie tylko czekać — umie też zmierzyć, ile klatek na sekundę faktycznie osiąga Twoja pętla, metodą zegar.get_fps(). Dodaj na końcu pętli, tuż po zegar.tick(FPS), jedną linię:
print(zegar.get_fps())
Uruchom i poczekaj sekundę. Na samym początku zobaczysz 0.0 — to normalne: get_fps() liczy średnią z ostatnich klatek i potrzebuje kilku, żeby się rozpędzić. Po chwili liczba ustabilizuje się blisko 8 — bo tick(8) trzyma tempo w ryzach. Jeśli widzisz tylko 0, daj programowi parę sekund — nie zamykaj okna za szybko.
Teraz najciekawsze — zdejmij hamulec i zobacz prawdziwą moc swojej maszyny. Zmień tymczasowo stałą na bardzo dużą wartość:
FPS = 5000 # tymczasowo — praktycznie bez limitu
Uruchom ponownie. Przy tick(5000) zegar prawie nie czeka, więc pętla biega na maksa, a terminal pokaże, ile klatek Twój komputer robi NAPRAWDĘ — często 2000, 3000, a nawet ponad 5000 na sekundę. To ta sama pętla, tylko bez metronomu. Po eksperymencie usuń print i przywróć FPS = 8.
FPS = 8 terminal po rozgrzewce pokazuje ~8 — zegar dotrzymuje słowa niezależnie od mocy komputera (te pierwsze 0.0 to tylko budowanie średniej). Przy FPS = 5000 hamulec znika i widzisz, ile klatek maszyna wyciąga sama z siebie — setki lub tysiące, na każdym komputerze inaczej. Właśnie tę niekontrolowaną prędkość tick() trzyma w ryzach, żeby gra działała identycznie na każdym sprzęcie.zegar.tick(FPS) na sam początek pętli (przed zdarzenia). Uruchom — gra działa. Dlaczego mimo to konwencja każe trzymać tick na końcu? Wróć do poprawnej wersji.ekran.fill() a pygame.display.flip() — i co by się stało, gdyby zabrakło każdego z nich.zegar.tick(FPS) — i wyjaśnić, jak tick wyrównuje czas klatkifill() (ghost effect) i po co flip() (bufor i migotanie)Przez trzy lekcje teorii budowałeś ten kod kawałek po kawałku: w T01 import, stałe i okno, w T02 trójfazową pętlę z obsługą klawiatury, w T03 zegar i kontrolę czasu. Teraz inaczej — napiszesz wszystko od zera, w jednym podejściu, bez podglądania poprzednich lekcji. Po co, skoro ten kod już masz? Bo między „rozumiem, gdy czytam” a „umiem napisać sam” jest przepaść, którą zasypuje tylko samodzielne pisanie. Ten szkielet to początek każdej gry Pygame, jaką kiedykolwiek stworzysz — po S01 masz go mieć w palcach.
Spróbuj najpierw z pamięci: stałe, inicjalizacja, pętla z trzema fazami, tick na końcu. Dopiero gdy utkniesz, spójrz na wzorzec poniżej. Jeśli przepiszesz całość bez zrozumienia — oszukasz tylko siebie.
snake.py. Usuń dotychczasową zawartość — piszesz od zera, na czysto.
Efekt: puste okno 600×600 z ciemnozielonym tłem i tytułem na pasku. Żadnych elementów — tylko tło.Wzorzec, do którego porównasz swój kod. Zwróć uwagę na komentarze dzielące plik na sekcje — STAŁE, INICJALIZACJA, GAME LOOP — od teraz każdy kod w tym kursie będzie tak zorganizowany:
import pygame
pygame.init()
# ─── STAŁE ────────────────────────────────────────────
SZEROKOSC = 600
WYSOKOSC = 600
FPS = 8
KOLOR_TLO = (15, 25, 15) # ciemnozielony
# ─── INICJALIZACJA ────────────────────────────────────
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE — S01: Okno")
zegar = pygame.time.Clock()
# ─── GAME LOOP ────────────────────────────────────────
while True:
# ZDARZENIA
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# LOGIKA # ← TUTAJ będziesz dodawał logikę gry
# RYSOWANIE
ekran.fill(KOLOR_TLO)
pygame.display.flip()
zegar.tick(FPS)
Porównaj swój kod ze wzorcem linia po linii. Jeśli coś napisałeś inaczej, ale działa — świetnie, ważna jest struktura, nie identyczność co do znaku. Zwróć jeszcze uwagę na komentarz # ← TUTAJ w sekcji LOGIKA: dziś to puste miejsce, ale za kilka lekcji będzie tam ruch węża, kolizje i zjadanie jabłek — kilkadziesiąt linii. Cała gra wyrośnie wewnątrz szkieletu, który właśnie napisałeś.
ModuleNotFoundError: No module named 'pygame' oznacza brak zainstalowanej biblioteki — wpisz pip install pygame i uruchom ponownie. SyntaxError ze wskazaniem numeru linii to literówka — porównaj tę linię ze wzorcem, najczęściej brakuje dwukropka albo nawiasu. A jeśli okno otwiera się i od razu znika bez błędu — sprawdź, czy pętla while True na pewno jest w pliku i czy kod rysowania jest wcięty do jej wnętrza.
python snake.py
W S01 zbudowałeś działające okno — pustą planszę o wymiarach 600 na 600 pikseli. Piksel to najmniejszy punkt, jaki ekran potrafi zapalić, a Twoje okno to krata 600 na 600 takich punktów. Żeby cokolwiek na nim narysować, musisz umieć wskazać konkretny punkt, i właśnie do tego służy układ współrzędnych.
Każdy punkt opisują dwie liczby. Pierwsza z nich, x, mówi, jak daleko w prawo; druga, y, jak daleko w dół. Punkt (0, 0) leży w lewym górnym rogu okna. I tu czeka pułapka, na której potyka się większość początkujących twórców gier: oś y rośnie w dół, odwrotnie niż na matematyce, gdzie y rośnie do góry. Dlaczego akurat tak? Bo ekrany od zawsze „kreślą” obraz od góry do dołu, linia po linii, trochę jak czytanie kartki. Dlatego y=0 to górna krawędź okna, a y=600 to sam dół.
Spójrz na schemat. Czerwony punkt to przykładowa pozycja (300, 300) — w połowie szerokości i w połowie wysokości, czyli środek okna. Skoro umiesz już wskazać punkt, potrzebujesz narzędzia, które coś w nim postawi. Tym narzędziem jest pygame.draw.rect. Przyjrzyj się jego budowie, argument po argumencie:
pygame.draw.rect(
ekran, # gdzie rysujemy — Twoj ekran z T01
(255, 0, 0), # kolor jako trojka RGB (tu czysta czerwien)
(300, 300, 20, 20) # (x, y, szerokosc, wysokosc) — pozycja i rozmiar
)
Trzy argumenty, jeden po drugim: powierzchnia, kolor i czwórka liczb. Kolor podajemy jako trzy liczby od 0 do 255 — pamiętasz z T01, że mówią one, ile w nim czerwieni, zieleni i błękitu. Dlatego (255, 0, 0) to czysta czerwień, a (0, 0, 255) czysty błękit. Z czwórki liczb dwie pierwsze to pozycja, dwie kolejne to rozmiar. Jest jednak szczegół, który zaraz Cię zaskoczy: punkt (x, y) wyznacza lewy górny róg prostokąta, a nie jego środek. Prostokąt rozrasta się od tego rogu w prawo i w dół. Dlatego znacznik o boku 20 pikseli musi zaczynać się najpóźniej na 580: dopiero wtedy zmieści się tuż przy prawej krawędzi (580 + 20 = 600, czyli dokładnie krawędź okna).
(R, G, B) — ile w nim czerwieni (Red), zieleni (Green) i błękitu (Blue). Każda liczba ma jasność od 0 (dioda wyłączona) do 255 (pełna moc). Stąd (0, 0, 0) to czarny, bo żadna dioda nie świeci; (255, 255, 255) to biały, bo wszystkie na maksimum; (255, 0, 0) to czysta czerwień, a (0, 0, 255) czysty błękit. Tło planszy Snake, (15, 25, 15), to bardzo ciemna zieleń — odrobina każdego koloru z przewagą zielonego.snake.py — masz w nim szkielet z S01: stałe, inicjalizację i trójfazowy game loop. W tej lekcji nie dokładasz jeszcze nic na stałe — tylko tymczasowo, na własne oczy, sprawdzisz układ współrzędnych.
Dodaj tymczasowo do sekcji RYSOWANIE (po ekran.fill(), a przed flip()) cztery znaczniki — po jednym w każdym rogu okna. Zanim uruchomisz, postaw hipotezę: który kolor wyląduje w którym rogu?
# Tymczasowo — test układu współrzędnych (usuń po lekcji)
pygame.draw.rect(ekran, (255, 0, 0), (0, 0, 20, 20)) # (0,0) lewy górny
pygame.draw.rect(ekran, (0, 0, 255), (580, 580, 20, 20)) # (580,580) prawy dolny
pygame.draw.rect(ekran, (255, 255, 0), (0, 580, 20, 20)) # (0,580) lewy dolny
pygame.draw.rect(ekran, (255, 0, 255), (580, 0, 20, 20)) # (580,0) prawy górny
Sprawdź swoje przewidywania. Czerwony usiadł w lewym górnym rogu, bo (0, 0) to początek układu. Niebieski na (580, 580) trafił w prawy dolny — oba duże, więc daleko w prawo i daleko w dół. Żółty na (0, 580) wylądował w lewym dolnym rogu, bo zerowe x trzyma go przy lewej krawędzi, a duże y spycha na sam dół. Magenta na (580, 0) zajął prawy górny róg — odwrotnie niż żółty: duże x niesie go w prawo, a zerowe y trzyma u góry. Jeśli żółty Cię zaskoczył, to znak, że intuicja matematyczna „duże y znaczy wysoko” wciąż w Tobie siedzi — w Pygame jest dokładnie odwrotnie. Te znaczniki są tylko po to, żeby zobaczyć współrzędne na żywo; usuń je na koniec, bo następną lekcję zaczynamy na czystym oknie.
Zwróć uwagę, jak wiele dało się zrobić jednym narzędziem: cztery wywołania draw.rect z różnymi liczbami postawiły cztery kwadraty w czterech różnych miejscach. Za każdym razem to dwie pierwsze liczby — x i y — decydowały, gdzie w oknie ląduje kwadrat, a reszta była taka sama. Opanowanie tej jednej zależności, że pozycja to para liczb, którą sam wybierasz, to połowa całego rysowania w grach.
x = 590 i boku 20 ostatnie 10 pikseli wystaje poza okno i ich nie zobaczysz. Zasada na przyszłość: jeśli kwadrat ma być w całości widoczny, x + bok nie może przekroczyć 600.
pygame.draw.rect(ekran, (255,0,0), (0, 0, 600, 600))?fill() zakryje tło; wywołany przed — sam zostanie zakryty. Czyli fill() to w gruncie rzeczy wygodny skrót dla „prostokąt na cały ekran”.Hipoteza na start: jakie wartości x i y postawią znacznik w prawym górnym rogu okna, w całości widoczny?
x = 595 przy boku 20 — 595 + 20 = 615. Co zobaczyłeś przy prawej krawędzi i dlaczego?(580, 0) (580 to ostatnia pozycja, w której bok 20 jeszcze mieści się w oknie: 600 − 20). Przy x = 595 znacznik jest przycięty — widać tylko 5 z 20 pikseli, bo reszta wychodzi poza okno. Pygame rysuje bez błędu, po prostu obcina to, co nie mieści się w kadrze.y = -10. Uruchom i opisz, dlaczego widać tylko jego kawałek. Przywróć poprawną wartość.W T04 nauczyłeś się trafiać w dowolny piksel planszy. To potężna umiejętność, ale dla węża zbyt dokładna: okno ma 600 na 600 pikseli, czyli 360 tysięcy osobnych punktów, a wąż nie potrzebuje takiej precyzji. On ma skakać po równych, sąsiadujących polach, a nie celować w pojedyncze punkty.
Dlatego grupujemy piksele w pola. Umawiamy się, że jedno pole to kwadrat o boku 20 pikseli, i zapisujemy tę umowę w stałej KRATKA = 20. Skoro plansza ma 600 pikseli szerokości, a pole 20, to mieści się ich 600 ÷ 20 = 30 w jednym rzędzie. Tyle samo w pionie — razem siatka 30 × 30, czyli 900 pól. Od tej chwili myślimy polami, a piksele zostają tylko przelicznikiem w tle.
Jak z numeru pola wyliczyć piksel? Reguła jest jedna i prosta. Pole o numerze (kolumna k, wiersz r) zaczyna się w pikselu (k×20, r×20). Pole (15, 15) zaczyna się więc w pikselu (300, 300) — dokładnie w środku planszy. Pole (0, 0) to piksel (0, 0), czyli lewy górny róg — z T04 wiesz, gdzie on jest i jak trafić w piksel.
Ten sam kwadrat możesz postawić w różnym polu — wystarczy zmienić numer pola, a piksel (x, y) policzy się sam. Inne pole to po prostu inne (x, y). I jedna rzecz nie zmienia się względem T04: (x, y) to wciąż lewy górny róg kwadratu. Dlatego głowa siedzi swoim rogiem dokładnie w rogu pola, a stamtąd wypełnia je w prawo i w dół.
snake.py z T04 — masz w nim czysty szkielet: stałe, inicjalizację i trójfazowy game loop, ze znacznikami testowymi już usuniętymi. W T04 dorysowywałeś kwadraty tylko tymczasowo. Teraz dodasz pierwszy obiekt gry na stałe: głowę węża.
Czas postawić pierwszy stały element gry. Dodasz dwie nowe stałe — KRATKA i KOLOR_GLOWA — ustawisz pozycję głowy na środku planszy i dorzucisz jedną linię draw.rect w sekcji RYSOWANIE. To pierwszy obiekt, który zostaje na ekranie na dobre, a nie tylko na czas testu.
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
KRATKA = 20 # rozmiar jednego pola siatki w pikselach
FPS = 8
KOLOR_TLO = (15, 25, 15)
KOLOR_GLOWA = (80, 255, 100) # jasny zielony
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE — T05: glowa")
zegar = pygame.time.Clock()
# pozycja startowa glowy — srodek planszy (pole 15,15 = piksel 300,300)
glowa_x = 300
glowa_y = 300
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
ekran.fill(KOLOR_TLO)
pygame.draw.rect(ekran, KOLOR_GLOWA, (glowa_x, glowa_y, KRATKA - 2, KRATKA - 2))
pygame.display.flip()
zegar.tick(FPS)
Na środku planszy siedzi jasnozielony kwadrat — pierwszy stały obiekt Twojej gry. Zatrzymaj się na chwilę nad tym, co jest tu zmienną, a co stałą. glowa_x i glowa_y to zmienne, bo opisują stan, który będzie się zmieniał — w T06 głowa ruszy z miejsca i te liczby zaczną rosnąć. KRATKA i kolory to stałe, czyli konfiguracja, która się nie zmienia w trakcie gry.
Głowa stoi w pikselu (300, 300), więc — jak już wiesz z przelicznika — siedzi w polu (15, 15), w samym środku siatki 30 × 30. Rogiem trzyma się rogu pola, a resztę pola wypełnia w prawo i w dół. To jest dokładnie ten obiekt, którym za chwilę zaczniesz sterować.
Pewnie zauważyłeś dziwny szczegół w kodzie — kwadrat ma bok KRATKA - 2, czyli 18, a nie pełne 20. Po co ta dwójka? Spójrz na tę jedną linię z osobna:
pygame.draw.rect(
ekran,
KOLOR_GLOWA, # kolor glowy (zielony)
(glowa_x, glowa_y, # x, y = lewy gorny rog pola (z T04)
KRATKA - 2, # szerokosc: 20 - 2 = 18 px
KRATKA - 2) # wysokosc: tak samo, 18 px
)
# te 2 px marginesu zostawiaja szczeline tla -> widac linie miedzy polami
Chodzi właśnie o tę widoczną linię między polami. Pole ma 20 pikseli, ale kwadrat zajmuje tylko 18 — zostają 2 piksele marginesu, przez które prześwituje ciemne tło. Przy jednym kwadracie różnicy prawie nie widać, ale gdy wąż urośnie i pól będzie wiele obok siebie, te szczeliny tła narysują cienkie linie siatki między segmentami. Dzięki temu wąż wygląda jak łańcuch osobnych ogniw, a nie jeden zlany pasek. Spójrz na schemat — od razu zobaczysz różnicę.
draw.rect stoi przed ekran.fill(), więc fill zamalowuje świeżo narysowany kwadrat tłem; rysowanie zawsze idzie po wypełnieniu tła. Druga — KRATKA ma za małą wartość i KRATKA - 2 wyszło 0 albo liczbę ujemną, a kwadrat o boku zero jest niewidoczny. Sprawdź kolejność linii i wartość stałej.
Hipoteza na start: czy odjęcie tych 2 pikseli w ogóle widać przy jednym kwadracie? A czy zobaczysz różnicę, gdyby pól było więcej?
KRATKA - 2 na samo KRATKA i uruchom. Przy jednym kwadracie różnicy prawie nie ma — zapamiętaj to na potem, gdy segmentów będzie wiele i margines zacznie rysować linie siatki.KRATKA - 10. Kwadrat robi się mały i zagubiony w polu — za duży margines psuje wygląd.KRATKA - 2 — to złoty środek między „zlanym paskiem” a „zagubioną kropką”.KRATKA - 10 zostawia za duży margines: kwadrat ginie w polu. KRATKA - 2 to dobry kompromis.ekran.fill(KOLOR_TLO) POD linię draw.rect. Uruchom, opisz, dlaczego głowa zniknęła, i przywróć poprawną kolejność.glowa_x jest zmienną, a KRATKA stałą — bez zaglądania do lekcji.draw.rectKRATKA - 2 i skąd biorą się widoczne linie między polamidx i dy, oraz jedną linię glowa_x += dx, i kwadrat ruszy z miejsca. Już teraz zapamiętaj jedną rzecz z T04: ruch „w górę” to będzie odejmowanie od y, bo oś y rośnie w dół. Za chwilę zobaczysz, jak bardzo to się przyda.
dx i dy, opisują „w którą stronę jest obrócona”, a pętla gry co klatkę robi jeden krok w tym kierunku. Sterowanie wężem to obracanie żółwia: zmiana tych dwóch liczb.
To lekcja, na którą pracowały trzy poprzednie: kwadrat z T05 wreszcie się ruszy, a wszystko sprowadzi się do prostej matematyki na współrzędnych z T04. Kierunek ruchu opiszesz dwiema liczbami: dx i dy — litera „d” jak delta, czyli zmiana. dx mówi, o ile zmienia się x w każdej klatce, a dy — o ile zmienia się y. Zanim ruszymy głową, przypomnij sobie dwa operatory, których zaraz użyjemy — += oraz -=:
# dodawanie: +=
x = 5
x += 1 # to samo co x = x + 1 -> x ma teraz 6
# odejmowanie: -=
y = 8
y -= 1 # to samo co y = y - 1 -> y ma teraz 7
# po prawej moze byc tez ZMIENNA, nie tylko liczba:
krok = 20
x += krok # to samo co x = x + krok -> x rosnie o 20
Czyli += dodaje do tego, co już jest w zmiennej, a -= odejmuje — to po prostu krótszy zapis tej samej operacji. Dlatego ruch głowy zapiszemy identycznie: glowa_x += dx to dokładnie to samo co glowa_x = glowa_x + dx, tylko zamiast jedynki dodajemy dx. Cały ruch to ta jedna linia powtarzana co klatkę — osobno dla x i osobno dla y.
Zamiast uczyć się czterech kierunków naraz, weźmiemy je po kolei — każdy z osobnym rysunkiem.
1. Kierunek w prawo. Zacznij od najprostszego: ustawiasz dx = +KRATKA (x rośnie o 20) przy dy = 0. Głowa z pola (300, 300) przeskakuje do sąsiedniego (320, 300):
KRATKA = 20 # jedno pole = 20 px (z T05)
dx = +KRATKA # +20: x rosnie -> ruch w prawo
dy = 0 # y bez zmian (pion)
glowa_x += dx # przesuwasz glowe o dx -> i jedziesz!
glowa_y += dy # dy = 0, wiec pion stoi
2. Kierunek w lewo. Tu jest odwrotnie: skoro x ma maleć, dajesz dx = -KRATKA (z minusem!), wciąż przy dy = 0. Ten sam start, ale głowa cofa się o jedno pole, do (280, 300):
3. Kierunek w dół. Teraz pion — i tu procentuje T04. W dół to dy = +KRATKA przy dx = 0. Uwaga na pułapkę z układu współrzędnych: y rośnie w dół, więc dodawanie do y spycha głowę niżej, do (300, 320):
4. Kierunek w górę. Góra to lustrzane odbicie dołu: dy = -KRATKA. Skoro y rośnie w dół, to odejmowanie od y unosi głowę do góry, do (300, 280). To klasyczna pułapka „każę w górę, a jedzie w dół” — ale Ty już wiesz, czemu w górę to minus:
Teraz, gdy rozumiesz każdy kierunek z osobna, zbierzmy je w jedną ściągę. Zwróć uwagę na prawidłowość: zawsze dokładnie jedna z dwóch liczb jest zerem — to definicja ruchu w klasycznym Snake, w którym wąż sunie poziomo albo pionowo, nigdy na skos.
| Kierunek | dx | dy | Dlaczego |
|---|---|---|---|
| prawo → | +KRATKA | 0 | x rośnie w prawo |
| lewo ← | -KRATKA | 0 | x maleje w lewo |
| dół ↓ | 0 | +KRATKA | y rośnie w dół! |
| góra ↑ | 0 | -KRATKA | y maleje w górę |
snake.py — głowa na (300, 300), kwadraty kontrolne usunięte. Dodasz dwie zmienne kierunku przed pętlą i dwie linie ruchu w sekcji LOGIKA.
Zaktualizuj kod do tej postaci — nowości to dx, dy przed pętlą oraz sekcja LOGIKA, która po raz pierwszy przestaje być pusta:
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
KRATKA = 20
FPS = 8
KOLOR_TLO = (15, 25, 15)
KOLOR_GLOWA = (80, 255, 100)
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE — T06: Ruch")
zegar = pygame.time.Clock()
glowa_x = 300
glowa_y = 300
dx = KRATKA # startujemy w prawo
dy = 0
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA — przesuń głowę ────────────────────
glowa_x += dx # x = x + KRATKA = 20 pikseli w prawo
glowa_y += dy # y = y + 0 = bez zmian
# ─── RYSOWANIE ─────────────────────────────────
ekran.fill(KOLOR_TLO)
pygame.draw.rect(ekran, KOLOR_GLOWA, (glowa_x, glowa_y, KRATKA - 2, KRATKA - 2))
pygame.display.flip()
zegar.tick(FPS)
Rusza się! Kwadrat startuje ze środka i równym tempem jedzie w prawo — osiem skoków na sekundę, każdy o jedną kratkę. Zatrzymaj się na moment przy tym, co właśnie widzisz: to pierwsza prawdziwa animacja w Twoim kursie. Pamiętasz flip-book z T02? Właśnie patrzysz na niego w akcji — każda klatka rysuje kwadrat w odrobinę innym miejscu, bo między klatkami glowa_x urosło o 20. Iluzja ruchu z nieruchomych obrazków.
Po kilku sekundach kwadrat znika za prawą krawędzią — i to na razie jest w porządku. Głowa przesuwa się bez sprawdzania granic: glowa_x rośnie powyżej 600, a Pygame, jak wiesz z T04, po prostu nie rysuje tego, co poza oknem. Kwadrat „istnieje” dalej — gdzieś na (700, 300), (720, 300)… — tylko go nie widać. Wykrywanie wyjścia poza planszę i kończenie gry to temat T18 i S08. Na razie celowo zostawiamy świat bez ścian.
glowa_x =+ dx to NIE to samo co glowa_x += dx. Odwrócone znaki Python czyta jako glowa_x = +dx, czyli „przypisz 20” — co klatkę od nowa. Głowa ląduje na x = 20 i już tam zostaje. Błąd jest podstępny, bo nie daje żadnego komunikatu — kolejność znaków sprawdzisz tylko wzrokiem: plus PRZED znakiem równości.
dx i dy, żeby jechał w dół?dx = 0, dy = KRATKA. Pamiętaj z T04: y rośnie W DÓŁ, więc dodatnie dy = ruch w dół.import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
KRATKA = 20
FPS = 8
KOLOR_TLO = (15, 25, 15)
KOLOR_GLOWA = (80, 255, 100)
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE — T06")
zegar = pygame.time.Clock()
glowa_x = 300
glowa_y = 300
dx = KRATKA
dy = 0
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
glowa_x += dx
glowa_y += dy
ekran.fill(KOLOR_TLO)
pygame.draw.rect(ekran, KOLOR_GLOWA, (glowa_x, glowa_y, KRATKA - 2, KRATKA - 2))
pygame.display.flip()
zegar.tick(FPS)
dx, dy jest zerem, a drugie ±KRATKA — i ta reguła wróci w T08, gdy będziemy pisać sterowanie.
Teraz, gdy głowa się rusza, masz dwa pokrętła: kierunek (dx/dy) i tempo (FPS). Sprawdź oba. Najpierw hipoteza: ile kratek na sekundę przejedzie głowa przy FPS=8? A przy FPS=16?
dx/dy na uruchomienie. Za każdym razem przewidź, za którą krawędzią kwadrat zniknie.FPS = 8 na 4, potem na 16. Obserwuj prędkość.FPS = 8 i kierunek w prawo — to stan wyjściowy do S02.dx = KRATKA * 2. Uruchom i opisz, co zmieniło się w ruchu — i dlaczego taki wąż psułby grę opartą na kratkach. Przywróć dx = KRATKA.glowa_x += dx i glowa_y += dy POZA pętlę (przed while True). Uruchom. Dlaczego kwadrat stoi? Co to mówi o roli pętli w animacji? Przywróć kod.dx i dy — i ustawić je dla każdego z 4 kierunków bez zaglądania do tabeliglowa_x += dx i dlaczego musi być w sekcji LOGIKA wewnątrz pętliZasada ta sama co w S01: pisz z pamięci, nie przepisuj. Tym razem masz więcej elementów do złożenia — przejdź je w głowie, zanim dotkniesz klawiatury. Szkielet znasz z S01: stałe, inicjalizacja, trójfazowa pętla. Do tego dochodzą nowe klocki: stała KRATKA i kolor głowy (T05), pozycja startowa na środku (T05), delty kierunku i ruch += w sekcji LOGIKA (T06) oraz draw.rect w RYSOWANIU. Jeśli pamiętasz po co jest każdy klocek, kolejność złoży się sama.
snake.py i usuń dotychczasową zawartość — piszesz od zera, na czysto. Wzorzec poniżej służy do porównania PO napisaniu własnej wersji.
import pygame
pygame.init()
# ─── STAŁE ────────────────────────────────────────────
SZEROKOSC = 600
WYSOKOSC = 600
KRATKA = 20 # rozmiar jednej kratki w pikselach
FPS = 8
KOLOR_TLO = (15, 25, 15)
KOLOR_GLOWA = (80, 255, 100)
# ─── INICJALIZACJA ────────────────────────────────────
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
pygame.display.set_caption("SNAKE — S02: Ruchoma głowa")
zegar = pygame.time.Clock()
# ─── POZYCJA STARTOWA ─────────────────────────────────
glowa_x = 300 # środek planszy w poziomie
glowa_y = 300 # środek planszy w pionie
# ─── KIERUNEK RUCHU ───────────────────────────────────
dx = KRATKA # startujemy w prawo: x rośnie o KRATKA co klatkę
dy = 0 # brak ruchu pionowego ← TUTAJ zmienisz na skręt (T07)
# ─── GAME LOOP ────────────────────────────────────────
while True:
# ZDARZENIA
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# LOGIKA — przesuń głowę
glowa_x += dx
glowa_y += dy
# RYSOWANIE
ekran.fill(KOLOR_TLO)
pygame.draw.rect(ekran, KOLOR_GLOWA,
(glowa_x, glowa_y, KRATKA - 2, KRATKA - 2))
pygame.display.flip()
zegar.tick(FPS)
Porównaj swoją wersję ze wzorcem. Drobne różnice w nazwach czy kolejności stałych nie mają znaczenia — liczy się struktura: stałe na górze, stan gry (pozycja, kierunek) przed pętlą, ruch w LOGICE, rysowanie po fill(). Zwróć uwagę na komentarz ← TUTAJ zmienisz na skręt (T07) przy dy — to dokładnie ten punkt, w którym za dwie lekcje strzałki przejmą kontrolę nad kierunkiem.
glowa_x += dx jest wewnątrz pętli, w sekcji LOGIKA — i czy dx na pewno wynosi KRATKA, a nie 0 (to dokładnie ta usterka, którą sam wywołałeś w zadaniu 3 z T06). Kwadrat mignął i znikł? Pewnie wystartował poza planszą — sprawdź pozycję startową (300, 300). Kwadratu w ogóle nie widać? Klasyka z T04: draw.rect musi stać PO ekran.fill(), inaczej tło go zamalowuje. A jeśli ruch jest, ale „klatkuje” nierówno — upewnij się, że zegar.tick(FPS) nie wypadł z końca pętli.
glowa_x += dx) jest w LOGICE, przed rysowaniem
To nie kartkówka, tylko lustro: pokaże, które lekcje warto odświeżyć. Pytania pojawiają się pojedynczo, po odpowiedzi od razu widzisz poprawną. Wynik poniżej progu? Zanotuj tematy, przy których się potknąłeś, wróć do tych lekcji — i losuj nowy zestaw.