25 lekcji — od pustego okna do pełnej gry: wąż, jabłka, punkty, Game Over i rosnące tempo. Każda lekcja to działający program.
Ten kurs wymaga znajomości podstaw Pythona. Od drugiej lekcji buduje grę ze zmiennych, warunku, pętli i funkcji, i nie zatrzymuje się, żeby tłumaczyć te pojęcia od zera. Jeśli znasz Pythona z naszych Podstaw Pythona albo z innego kursu, jesteś w dobrym miejscu. Jeśli te cztery pojęcia nic Ci nie mówią, zacznij od Podstaw. Tam poznasz je w przeglądarce, a Snake poczeka.
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ąż z ciałem rośnie po każdym zjedzonym jabłku, gra przyspiesza razem z punktami, po zderzeniu ze ścianą albo z własnym ciałem pojawia się ekran KONIEC GRY z Twoim wynikiem, a klawisz R daje drugą szansę. A pętla gry i obsługa zdarzeń, 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 25 lekcji. To materiał na kilkanaście godzin spokojnej nauki. Lekcja lub dwie dziennie to świetny rytm: liczy się regularność, nie tempo.
Każda lekcja prowadzi Cię krokami: czytasz jeden krok, klikasz Dalej i odsłania się następny. W krokach czekają ćwiczenia: piszesz kod prosto w przeglądarce, klikasz Sprawdź i od razu dostajesz werdykt. Tych ćwiczeń nie da się przeskoczyć, bo dopiero ich zaliczenie otwiera następną lekcję. To celowe: w tym kursie idziesz dalej dlatego, że umiesz, a nie dlatego, że doczytasz stronę do końca. Między „rozumiem, gdy czytam” a „umiem napisać” jest przepaść i właśnie te ćwiczenia ją zasypują.
Przez cały kurs pracujesz w jednym pliku: snake.py. Na starcie ma pięć linii i pokazuje samo okno, na końcu ma 151 linii pełnej gry, a każda lekcja dokłada kolejne do tego, co już masz. Niczego przy tym nie instalujesz: Python i Pygame są wbudowane w stronę, więc piszesz i uruchamiasz grę w przeglądarce od pierwszej minuty. Pod kodem każdej lekcji stoi panel 🐍 Plik lekcji: snake.py, z którego jednym kliknięciem uruchomisz grę albo pobierzesz plik na dysk, a pod oknem gry Konsola pokazuje wszystko, co program wypisze. Instalacja u siebie to wybór na później, nie warunek startu.
R.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.Zwykle kurs Pythona zaczyna się od pobrania instalatora i klikania „dalej”. Tutaj nie. Python, którego będziesz używać, jest wbudowany w tę stronę i uruchamia się w Twojej przeglądarce, razem z Pygame, biblioteką do obsługi okien i grafiki.
Pod tym tekstem czeka przycisk ▶ Uruchom. Otworzy on Kokpit, czyli okno, w którym przez cały kurs będziesz uruchamiać swoje programy, i włączy w nim mały program pokazowy. Kokpit pojawi się na wierzchu lekcji i zasłoni tekst. Tak ma być: to okno zostaje tam, gdzie je ustawisz, a ta lekcja uczy, jak nad nim zapanować. Pierwsze uruchomienie potrwa chwilę, bo cały Python musi się najpierw pobrać do przeglądarki.
Zanim klikniesz, zapamiętaj jedno. Na górnym pasku Kokpitu jest przycisk Zwiń. Chowa on Kokpit do zakładki przy krawędzi kursu i z powrotem odsłania tekst. Kliknij ▶ Uruchom, a gdy Kokpit zasłoni lekcję, kliknij Zwiń na jego pasku. Tekst wróci, a pod tym krokiem pojawi się przycisk Dalej.
Zwinięcie niczego nie zatrzymało. Program w Kokpicie cały czas działa, tylko go nie widać. Zakładka przy krawędzi kursu przywołuje Kokpit z powrotem, a ten znów zasłoni tekst. Dlatego najpierw doczytaj do końca, a dopiero potem klikaj. Na górze Kokpitu jest pasek z jego nazwą. Za ten pasek możesz złapać Kokpit i przesunąć go w bok, tam, gdzie nie zasłania tekstu. Zostanie w tym miejscu również w następnych lekcjach.
Kliknij zakładkę, a gdy Kokpit wróci, złap go za pasek i odsuń na bok. Gdy znajdzie się obok tekstu, pod krokiem pojawi się przycisk Dalej.
W Kokpicie jest drugie, mniejsze okno: okno pokazu. To prawdziwe okno Pygame. Ma własny pasek z nazwą pokaz.py i krzyżykiem ✕, tak jak każde inne okno na komputerze, a istnieje dokładnie tak długo, jak długo działa program. Jeszcze go nie zamykaj. Przyjrzyj się paskowi okna pokazu, znajdź na nim krzyżyk i kliknij Dalej.
Na tej różnicy opiera się cała lekcja: Zwiń tylko chowa Kokpit, a krzyżyk kończy program. Gdy klikniesz ✕, to nie Ty zamkniesz okno. Program zauważy krzyżyk i sam się zakończy, a okno zniknie razem z nim. Twój pierwszy własny program jeszcze nie będzie tego potrafił. Nauczysz go tego na trzeciej lekcji.
Zamknij pokaz krzyżykiem. Okno zniknie, Kokpit zostanie na swoim miejscu, a pod krokiem pojawi się przycisk Dalej.
W Kokpicie zawsze pracujesz na jednym pliku. W tej lekcji był to pokaz, czyli pokaz.py. Od następnej będzie to snake.py, czyli Twoja gra w takim stanie, do jakiego doprowadziła ją lekcja, którą właśnie czytasz. Nie musisz jeszcze rozumieć ani jednej linii pokazu. Wrócą do Ciebie po kolei, każda w swojej lekcji. Czas na pierwszą linię: pod tym krokiem czeka przycisk do drugiej lekcji.
import pygame
pygame.init()
ekran = pygame.display.set_mode((600, 600))
zegar = pygame.time.Clock()
print("Pokaz wystartowal - to okno rysuje Python.")
x = 60
szybkosc = 4
dziala = True
while dziala:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
dziala = False
x = x + szybkosc
if x <= 0 or x >= 560:
szybkosc = -szybkosc
ekran.fill((15, 25, 15))
pygame.draw.rect(ekran, (80, 220, 120), (x, 280, 40, 40))
pygame.display.flip()
zegar.tick(60)
print("Do zobaczenia!")
pygame.quit()
Policz ze mną. Pięć linii. Nie pięćdziesiąt i nie pięćset. Pięć. Wszystkie poznasz w tej lekcji, jedną po drugiej, i każda będzie miała swój powód.
Zanim zaczniesz, odpowiedz na jedno pytanie. Skąd program w ogóle bierze okno gry? Przecież okno musi umieć się otworzyć, przesunąć, zareagować na mysz.
Spójrz na rysunek. Taki zestaw gotowych narzędzi to biblioteka. Jedną bibliotekę już znasz. To random z Podstaw, z której pochodzi gotowe random.randint().
Twój snake.py zaczyna się od dwóch linii. Pusta linia między nimi jest dla oka, bo Python ją pomija:
import pygame
pygame.init()
import pygame dołącza narzędzia do Twojego programu, dokładnie tak samo, jak import random dołączał funkcje do losowania. Pierwszą funkcją z pygame, której potrzebujesz, jest init(). To ona uruchamia całą resztę, bo biblioteka sprawdza wtedy, co ma do dyspozycji, i przygotowuje się do pracy.
Przeczytaj pygame.init() od lewej, jak ścieżkę: z pakietu pygame weź funkcję init i ją uruchom. Kropka to drogowskaz „szukaj w środku", a nawiasy na końcu to wywołanie tej funkcji, czyli polecenie, żeby ją wykonać.
Ten sam zapis znasz z random.randint(): z pakietu random, funkcja randint.
Trzecia linia otwiera okno o wymiarze 600 na 600 pikseli:
ekran = pygame.display.set_mode((600, 600))
Ścieżka jest tu o jedno piętro głębsza. Z pakietu pygame bierzesz moduł display, a z niego funkcję set_mode. Pakiet to inne słowo na bibliotekę, czyli cały zestaw narzędzi, który dołączasz przez import. Moduł to jeden dział w tym zestawie. display zajmuje się oknem, inne działy poznasz w kolejnych lekcjach. Każda kropka w zapisie to zejście o piętro niżej: zestaw, dział, narzędzie. Nawias wygląda podwójnie i to nie jest pomyłka. Zewnętrzny nawias to zwykłe wywołanie funkcji. Wewnętrzny to para liczb, szerokość i wysokość okna w pikselach, 600 na 600. Taką parę zapisaną w nawiasie znasz z Podstaw jako krotkę. Wrócimy do niej porządnie, gdy przyjdzie czas budować węża.
Gotowe okno program zapamiętuje pod nazwą ekran. Ta nazwa przyda się, gdy zaczniemy po nim rysować.
Trzy linie są na miejscu i to jest już prawdziwy program, Twój plik snake.py. Właśnie dlatego wrócił Kokpit, ten sam, który znasz z poprzedniej lekcji, tylko na pasku ma teraz snake.py. Od tej chwili zawsze pokazuje aktualny stan tego pliku. W nim program się uruchamia, w nim pojawia się okno i w nim go zatrzymasz, gdy przyjdzie pora. Zrób pierwszy bieg: kliknij przycisk ▶ Uruchom pod kodem i patrz na Kokpit, bo będzie szybko.
Ten sposób już znasz. while True: to pętla z Podstaw, która kręci się bez końca, bo jej warunek jest zawsze prawdziwy. Tam bywała pułapką; tutaj dostaje nową, poważną rolę. Dopóki pętla się kręci, program działa, a dopóki program działa, okno wyświetla się na ekranie. Prawie każda gra na świecie ma w środku takie serce, które bije od uruchomienia aż do wyjścia. Zostało ustalić, co ono ma robić w każdym obiegu.
Ma pokazywać obrazek. Robi to pygame.display.flip(): program składa całą klatkę w ukryciu, a flip() wystawia ją na ekran jednym ruchem, w całości. Dzięki temu nigdy nie widzisz obrazu złożonego w połowie.
Jedno wywołanie to jeden obrazek. A gra nie jest obrazkiem, tylko filmem.
Pomyśl o zeszycie, w którego rogu rysujesz ludzika. Na każdej kartce jest trochę inaczej: raz z ręką w górze, raz w połowie drogi, raz przy nodze. Sama kartka to nieruchomy rysunek. Dopiero kiedy przewracasz kartki kciukiem, jedna po drugiej, ludzik zaczyna machać. Ruch nie tkwi w żadnej kartce, tylko w tym, że lecą jedna po drugiej.
Twój program robi dokładnie to samo: flip() pokazuje jedną kartkę, a pętla przewraca je bez końca, kilka razy na sekundę. Dlatego pętla musi tu być. Bez niej byłby jeden obrazek i koniec filmu. Dziś kartka jest pusta i czarna, więc zobaczysz po prostu czerń. Tak ma być, bo oglądasz samą pracę pętli, jeszcze bez niczego narysowanego.
Piąta linia jest już na swoim miejscu i Twój plik wygląda tak. Zwróć uwagę na jedną rzecz, bo od niej zależy wszystko:
import pygame
pygame.init()
ekran = pygame.display.set_mode((600, 600))
while True:
pygame.display.flip()
Linia z flip() zaczyna się od czterech spacji i to one mówią Pythonowi, że należy do pętli. Gdyby była przy lewej krawędzi, bez wcięcia, program wykonałby ją dopiero po pętli, czyli nigdy, bo z pętli bez końca nic nie wychodzi. O tym, co jest w środku, a co na zewnątrz, decyduje wyłącznie wcięcie.
Pięć linii, komplet. Czas na eksperyment. Za chwilę uruchomisz program i spróbujesz zamknąć okno tak, jak zamyka się każde inne: krzyżykiem na jego pasku. Klikaj śmiało, nawet kilka razy. System może po chwili dopisać na pasku tytułu, że program nie odpowiada, i tak właśnie ma być po tej lekcji. Kiedy skończysz eksperymentować, przerwij program przyciskiem ⏹ Zatrzymaj program w Kokpicie. Dlaczego kliknięcia trafiają w próżnię, rozgryziesz potem, tuż pod spodem. Zapamiętaj to doświadczenie, bo to najczęstsza przyczyna zamrożonych okien u początkujących. A w Twoich pięciu liniach nie ma błędu. Brakuje im jednej rzeczy. Teraz uruchom program i klikaj w krzyżyk.
import pygame
pygame.init()
ekran = pygame.display.set_mode((600, 600))
while True:
pygame.display.flip()
Zagadka krzyżyka zostaje otwarta. Rozwiążesz ją na następnej lekcji. Zanim tam pójdziesz, podsumujmy, co udało się dziś zbudować. import pygame dołącza bibliotekę, pygame.init() inicjuje potrzebne moduły z pygame, pygame.display.set_mode((600, 600)) otwiera okno o zadanym rozmiarze. while True: trzyma program przy życiu, a pygame.display.flip() w każdym obiegu pokazuje gotową klatkę. Twój plik wygląda teraz dokładnie tak:
import pygame
pygame.init()
ekran = pygame.display.set_mode((600, 600))
while True:
pygame.display.flip()
Na następnej lekcji dołożysz do niego cztery linie. Po nich okno przestanie być głuche. Zacznie słuchać, co się dzieje, i zamknie się krzyżykiem jak każde porządne okno.
Masz działające okno: czarne, stabilne, Twoje. W tej lekcji dowiesz się, dlaczego jest głuche. A kiedy to zrozumiesz, naprawisz je własnymi rękami.
Zacznij od słowa, na którym opiera się cała ta lekcja. Spójrz na rysunek. Kliknięcie krzyżyka Twojego okna to też zdarzenie i właśnie ono nas dziś interesuje.
Jest jedna rzecz, która czyni zdarzenia wygodnymi. System nie przerywa Twojego programu w pół zdania, żeby mu o nich opowiedzieć. Zamiast tego odkłada każde zdarzenie do kolejki, jedno za drugim, w tej kolejności, w jakiej się wydarzyły. Tak jak z listami w skrzynce pocztowej. Listonosz nie wpada do mieszkania, tylko zostawia list w skrzynce, a Ty wyjmujesz pocztę wtedy, kiedy zechcesz. Program działa tak samo. Kolejka czeka, aż program po nią sięgnie. Spójrz na rysunek.
Teraz masz wszystko, żeby rozwiązać zagadkę z poprzedniej lekcji. Twój program ani razu nie sięgnął do kolejki. Kręcił pętlę i pokazywał klatki, a każde Twoje kliknięcie w krzyżyk system posłusznie odkładał do kolejki, gdzie leżało i czekało. Kolejka rosła, odpowiedź nie przychodziła, aż system uznał program za niereagujący i dopisał ostrzeżenie do paska tytułu. Program nie był zepsuty. On po prostu nigdy nie zapytał, co się wydarzyło.
Pora nauczyć program pytać. Służy do tego pygame.event.get(). Przeczytaj ten zapis od lewej: z pakietu pygame, z modułu event (po angielsku: zdarzenie), funkcja get, czyli „daj". To wywołanie sięga do kolejki i zwraca listę wszystkiego, co wydarzyło się od poprzedniego pytania. Po liście umiesz przejść pętlą for z Podstaw. Ta linia wchodzi do wnętrza while True:, nad pygame.display.flip():
for zdarzenie in pygame.event.get():
Zmienna zdarzenie przyjmuje w każdym przebiegu jedno zdarzenie z listy. Co z nim zrobić, dowiesz się w następnym kroku.
Zdarzeń przez kolejkę płynie wiele, a Ciebie interesuje na razie jedno. Każde zdarzenie niesie swój rodzaj, zapisany pod zdarzenie.type, a rodzaj oznaczający kliknięcie krzyżyka nazywa się pygame.QUIT (po angielsku: wyjść, zakończyć). Wystarczy więc porównanie, które znasz od pierwszych lekcji Podstaw:
if zdarzenie.type == pygame.QUIT:
Ten warunek jest wewnątrz pętli for, bo sprawdzamy każde zdarzenie z osobna.
Zostało powiedzieć programowi, co ma zrobić, gdy warunek się spełni. W Pygame program kończy się zawsze tymi samymi dwiema liniami. Potraktuj je jak stały zwrot, którego używa się w całości: pygame.quit() zamyka okno i wyłącza bibliotekę, a raise SystemExit kończy sam program. Wystarczy wiedzieć, że ta para kończy program czysto. Cała pętla wygląda teraz tak:
while True:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
pygame.display.flip()
Okno pokaże się takie samo jak wcześniej: czarne, spokojne. Cała różnica jest w krzyżyku. Kliknij go i patrz na dwie rzeczy: co zrobi okno i czy na pasku tytułu pojawi się dopisek, że program nie odpowiada. Uruchom program w Kokpicie.
Okno znika. Bez awaryjnego zatrzymywania, bez klikania ⏹. Program kończy pracę sam, tak jak każdy porządny program. Dopisku też nie ma. System przestał się skarżyć, bo program w każdym obiegu pętli sięga teraz do kolejki i odpowiada na to, co w niej czeka.
Przez kolejkę płynie znacznie więcej niż krzyżyk: każdy ruch myszy, każde naciśnięcie i puszczenie klawisza. Program wybiera z tego strumienia tylko te rodzaje zdarzeń, na które chce reagować, i resztę po prostu pomija.
W tych czterech liniach najłatwiej pomylić się w jednym miejscu: we wcięciach. Wcięcie mówi Pythonowi, co do czego należy, więc przesunięcie jednej linii o poziom zmienia sens programu, choć wygląda on prawie tak samo. Dwa objawy warto umieć rozpoznać. Jeśli sprawdzenie rodzaju zdarzenia wyląduje poza pętlą for, program przestaje reagować na krzyżyk albo kończy się błędem zaraz po starcie. Jeśli za to zamknięcie wyląduje poza warunkiem if, program zamyka się od byle czego. Wystarczy ruszyć myszą nad oknem i już go nie ma, bo każde zdarzenie, nie tylko krzyżyk, uruchamia wyjście. Gdy zobaczysz któryś z tych objawów, nie szukaj literówki. Sprawdź wcięcia.
Teraz Twoja kolej. W ćwiczeniach pod lekcją złożysz obsługę zdarzeń samodzielnie, z pamięci, nie z podglądania. Jeśli coś nie zadziała, wróć do kroku o wcięciach, bo najczęstszy winowajca mieszka właśnie tam.
import pygame
pygame.init()
ekran = pygame.display.set_mode((600, 600))
while True:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
pygame.display.flip()
Zobacz, co umiesz. Wiesz, czym jest zdarzenie i że system odkłada zdarzenia do kolejki, a program po nie sięga. Umiesz zapytać o nie przez pygame.event.get(), rozpoznać kliknięcie krzyżyka po pygame.QUIT i czysto zakończyć program parą pygame.quit() oraz raise SystemExit. Twój plik wygląda teraz dokładnie tak:
import pygame
pygame.init()
ekran = pygame.display.set_mode((600, 600))
while True:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
pygame.display.flip()
Ten plik zaraz uruchomisz w Kokpicie, a potem zamkniesz okno krzyżykiem ✕. Po raz pierwszy Twoja gra skończy się tak, jak każdy porządny program. Uruchom go i zamknij krzyżykiem.
Spójrz na pętlę jeszcze raz. Ma już dwie wyraźne części: najpierw zbiera zdarzenia, potem pokazuje klatkę. To dwie z trzech faz, z których składa się każda gra. Na następnej lekcji wszystkie trzy dostaną nazwy, a pętla dostanie równe, kontrolowane tempo.
Twoja pętla kręci się bez przerwy: zbiera zdarzenia, pokazuje klatkę, i od nowa. Pytanie na start: ile pełnych obiegów robi w ciągu jednej sekundy? Dziesięć? Sto? Więcej? Zanim przejdziesz dalej, zatrzymaj się i zapisz swój typ, choćby na kartce. Obstawiasz w ciemno i o to właśnie chodzi, bo za chwilę porównasz.
Odpowiedź brzmi: tyle, ile pozwoli procesor. Na jednym komputerze będzie to kilkaset obiegów na sekundę, na innym kilka tysięcy. Pętla nie ma żadnego hamulca, więc pędzi na pełnej mocy maszyny, na której działa. A każda maszyna ma tę moc inną. I tu pojawia się prawdziwy problem: ten sam program biegnie u każdego gracza w innym tempie. Gra, która u Ciebie jest grywalna, na szybszym komputerze może być kilkanaście razy szybsza i nie do opanowania. Tempo gry nie może zależeć od komputera. Musi je wyznaczać ktoś inny.
Wyobraź sobie zespół muzyczny bez perkusisty: każdy gra we własnym tempie i utwór po kilku taktach się rozjeżdża. Dlatego muzycy ćwiczą z metronomem, czyli urządzeniem, które tyka idealnie równo i wszystkim narzuca wspólne tempo. Metronom nie gra za nikogo; po prostu wybija takt, a cała reszta się do niego dostraja. Dokładnie takiego metronomu potrzebuje Twoja pętla.
Zanim wstawisz metronom, uporządkuj samą pętlę. Każdy jej obieg to w istocie trzy rodzaje pracy, zawsze te same. Odsłaniaj je na rysunku po kolei, klikając blok po bloku.
Kolejność tych trzech bloków nie jest przypadkowa i nie wolno jej mieszać. Zamiast wykładu zrobimy eksperyment: poprzestawiaj bloki na rysunku kliknięciem i zobacz, co się psuje przy każdym ustawieniu.
Teraz przenieś ten porządek do swojego pliku. Nie dopisujesz ani jednej działającej linii. Dodajesz trzy podpisy, żeby każdy blok miał w pętli swoje miejsce z nazwą:
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
# ─── RYSOWANIE ───────────────────────────────
pygame.display.flip()
Podpisy zaczynają się od #, więc Python je pomija, ale Tobie będą służyć w każdej kolejnej lekcji, bo od razu widać, gdzie co wstawić. Blok LOGIKA jest na razie pusty i to jest w porządku. Za kilka lekcji pojawi się w nim ruch; puste miejsce nie jest brakiem, tylko zarezerwowanym stolikiem.
Czas na metronom. Powstaje jedną linią, dopisaną przed pętlą, zaraz pod linią z oknem:
zegar = pygame.time.Clock()
Przeczytaj od lewej: z pakietu pygame, z modułu time, Clock, po angielsku zegar. To wywołanie tworzy zegar, a program zapamiętuje go pod nazwą zegar. Linia jest przed pętlą nie bez powodu, bo metronom wystarczy zbudować raz. Gdyby stała w środku, każdy obieg tworzyłby nowy zegar, a nowy zegar niczego jeszcze nie odmierzył.
Sam zegar niczego jeszcze nie zmienia. Trzeba mu kazać tykać. Robi to ostatnia linia pętli, dopisana pod pygame.display.flip():
zegar.tick(8)
Przeczytaj od lewej: Twój zegar, jego tykanie, czyli tick, osiem razy na sekundę. Przy każdym obiegu tick(8) mierzy, ile trwała bieżąca klatka, i jeśli poszło szybciej niż jedna ósma sekundy, każe programowi odczekać resztę. Efekt jest taki, że każda klatka trwa równo jedną ósmą sekundy, na każdym komputerze tak samo. To dokładnie osiem obiegów na sekundę zamiast setek czy tysięcy z Twojego obstawiania. Dlaczego akurat osiem? Nasza gra będzie się poruszać skokami, a osiem skoków na sekundę to tempo, przy którym gracz ma na reakcję około jednej ósmej sekundy. To dość, żeby zdążyć, i za mało, żeby się nudzić.
Pora utrwalić. W ćwiczeniach pod lekcją poskładasz silnik samodzielnie, z pamięci i ze zrozumienia, nie z przepisywania.
import pygame
pygame.init()
ekran = pygame.display.set_mode((600, 600))
zegar = pygame.time.Clock()
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
# ─── RYSOWANIE ───────────────────────────────
pygame.display.flip()
zegar.tick(8)
Zobacz, co umiesz. Wiesz, że pętla bez hamulca kręci się tak szybko, jak pozwoli procesor, i że tempo gry nie może od tego zależeć. Znasz trzy fazy każdego obiegu: ZDARZENIA, LOGIKA, RYSOWANIE. Wiesz też, dlaczego są w tej kolejności. Umiesz zbudować zegar przez pygame.time.Clock() i nadać grze równe tempo przez zegar.tick(8). Twój plik wygląda teraz dokładnie tak:
import pygame
pygame.init()
ekran = pygame.display.set_mode((600, 600))
zegar = pygame.time.Clock()
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
# ─── RYSOWANIE ───────────────────────────────
pygame.display.flip()
zegar.tick(8)
To jest kompletny silnik gry: pętla, która słucha, ma miejsce na myślenie i pokazuje klatki w równym rytmie. Na następnej lekcji plansza dostanie kolor, a nienazwane liczby w Twoim pliku, czyli 600, 600 i 8, dostaną wreszcie nazwy.
W Twoim pliku są trzy gołe liczby: 600, 600 i 8. Używasz ich od trzech lekcji i wpisujesz je, bo tak kazaliśmy. Dwie sześćsetki są w rozmiarze okna, ósemka w tykaniu zegara. Nikt Ci dotąd nie powiedział, czym właściwie są ani dlaczego wolno im tak po prostu leżeć w kodzie bez podpisu. Dziś ten dług wraca do spłaty. Każda z tych liczb dostanie nazwę, która mówi, czym jest. A przy okazji plansza, od pierwszego okna czarna, dostanie kolor wybrany naprawdę, a nie z przypadku.
Liczba w kodzie nie mówi o sobie nic. Kiedy widzisz 600, nie wiesz, czy to szerokość okna, wysokość, czy może jakaś zupełnie inna wielkość. Wiesz tylko tyle, że to sześćset czegoś. Nazwa mówi wszystko. Nad resztą kodu postaw trzy linie, które nadają Twoim liczbom imiona, a niżej używaj imion zamiast liczb:
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
FPS = 8
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
Na dole pliku ósemka też ustępuje miejsca nazwie:
# ─── RYSOWANIE ───────────────────────────────
pygame.display.flip()
zegar.tick(FPS)
Taką nazwę, ustawianą raz na górze pliku i używaną w wielu miejscach, nazywa się stałą. FPS to skrót od angielskiej nazwy „klatek na sekundę". Mówi, ile klatek obrazu program pokazuje w każdej sekundzie, czyli dokładnie to, co dotąd znaczyła goła ósemka w tick.
Wielkie litery w nazwie to sygnał dla każdego, kto czyta kod: ta wartość jest ustawiana raz i do końca programu się nie zmienia. Jedna rzecz jest tu ważna. To umowa programistów, nie reguła języka. Python tego nie pilnuje. Gdyby w środku programu pojawiła się linia SZEROKOSC = 700, język nie zaprotestuje ani słowem i posłusznie podmieni wartość. Wielkie litery niczego nie wymuszają. One obiecują. Czytelnik Twojego kodu, a za tydzień tym czytelnikiem jesteś Ty, widzi SZEROKOSC i wie bez sprawdzania, że to jest ustalone raz i że tego nie trzeba śledzić po całym pliku.
Zaraz uruchomisz program w Kokpicie i porównasz okno, które tam się pojawi, z tym, które pamiętasz z poprzedniej lekcji. Zdradzę od razu: nie zobaczysz żadnej różnicy, i tak ma być. Nazwa nie zmienia tego, co program robi; zmienia to, jak się go czyta. Zysk zobaczysz przy pierwszej przebudowie. Gdyby plansza miała urosnąć do 800 na 800, poprawiasz dwie sąsiednie linie na samej górze pliku i gotowe. Bez stałych czekałoby Cię polowanie na wszystkie sześćsetki rozsiane po kodzie i zgadywanie przy każdej z osobna, czy to rozmiar planszy, czy zupełnie inna liczba, która tylko wygląda tak samo. Plik będzie rósł z każdą lekcją, więc ta różnica będzie rosła razem z nim. Uruchom program i porównaj okna.
Plansza jest czarna. Ale czerń nie była niczyim wyborem. To jest to, co zostaje, kiedy nikt nie powiedział, jak ma być. Twój program dostał rozkaz otwarcia okna o zadanym rozmiarze i na tym rozkazy się skończyły; o kolorze nie usłyszał ani słowa, więc pokazuje puste nic. Czas powiedzieć mu, jaka ta plansza ma być. Najpierw jednak trzeba wiedzieć, jak w ogóle mówi się komputerowi o kolorze, bo komputer nie zna słów „ciemnozielony" ani „granatowy".
Każdy kolor na ekranie komputer składa z trzech świateł: czerwonego, zielonego i niebieskiego. Odsłaniaj składowe na rysunku jedną po drugiej.
Kolor naszej planszy wygląda tak:
KOLOR_TLO = (15, 25, 15)
Trzy liczby podróżują razem, w jednym nawiasie, tak samo jak para 600 i 600 przy rozmiarze okna. Przeczytaj je suwakami z poprzedniego kroku: piętnaście czerwonego, dwadzieścia pięć zielonego, piętnaście niebieskiego. Wszystkie trzy światła ledwo się tlą, a zielone pali się odrobinę mocniej od pozostałych, więc wychodzi z tego prawie czarna zieleń. Ten wybór nie jest przypadkowy. Na tak ciemnym tle jasny wąż będzie widoczny od pierwszego spojrzenia, a plansza nie zmęczy oczu nawet po pół godzinie gry. I spójrz na nazwę. Wielkie litery są jak w kroku o stałych, bo kolor tła ustawiamy raz i się go trzymamy.
Stała z kolorem sama nic nie maluje, więc trzeba jej użyć. Otwórz fazę RYSOWANIE i nad flip() dopisz jedną linię:
# ─── RYSOWANIE ───────────────────────────────
ekran.fill(KOLOR_TLO)
pygame.display.flip()
zegar.tick(FPS)
Przeczytaj nową linię tak, jak czyta się zegar.tick(8): Twój ekran, jego wypełnienie. Przed kropką jest tu nie nazwa pakietu, jak w pygame.display.flip(), tylko nazwa rzeczy, którą Twój program sam stworzył i trzyma pod nazwą ekran. A kolejność w fazie RYSOWANIE ma sens, który znasz z w02: fill maluje całą planszę jednym kolorem na ukrytej klatce, flip pokazuje gotową klatkę jednym ruchem. Najpierw malujesz, potem pokazujesz. Uruchom. Okno, które od trzech lekcji było czarne, jest ciemnozielone.
Linia ekran.fill(KOLOR_TLO) jest wewnątrz pętli, więc tło jest malowane od nowa w każdym obiegu, czyli osiem razy na sekundę. Może Cię kusić pytanie: po co, skoro wystarczyłoby raz, przed pętlą? Uczciwa odpowiedź jest taka, że dziś różnicy nie zobaczysz. Na planszy nic się nie rusza, więc klatka malowana raz i klatka malowana co obieg wyglądają identycznie. Ale faza RYSOWANIE ma w tym kursie żelazną umowę: każdy obieg maluje całą klatkę od zera. Najpierw tło, potem wszystko, co na nim leży. Dowód, że ta umowa jest potrzebna, dostaniesz w lekcji, w której głowa węża zacznie jechać. Bez przemalowanego tła każda klatka zostawiałaby na planszy ślad poprzedniej i głowa ciągnęłaby za sobą smugę przez pół ekranu.
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
FPS = 8
KOLOR_TLO = (15, 25, 15)
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
# ─── RYSOWANIE ───────────────────────────────
ekran.fill(KOLOR_TLO)
pygame.display.flip()
zegar.tick(FPS)
To umiesz po tej lekcji: nadajesz liczbom nazwy i wiesz, że wielkie litery w nazwie to obietnica „ustawione raz, niezmienne"; zapisujesz kolor jako trójkę liczb od 0 do 255; malujesz tło całej planszy jedną linią w fazie RYSOWANIE. Twój plik wygląda teraz tak:
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
FPS = 8
KOLOR_TLO = (15, 25, 15)
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
# ─── RYSOWANIE ───────────────────────────────
ekran.fill(KOLOR_TLO)
pygame.display.flip()
zegar.tick(FPS)
Na następnej lekcji narysujesz na tej planszy pierwszy kwadrat i dowiesz się, jak program adresuje każdy punkt ekranu: gdzie leży zero i w którą stronę rosną liczby.
Popatrz na obrazek. Dokładnie coś takiego postawisz dziś we własnym pliku. Ale zanim padnie choć jedna linia kodu, obstaw odpowiedź na pytanie: ile liczb trzeba podać programowi, żeby narysował dokładnie ten kwadrat? Nie mniej i nie więcej, tylko tyle, ile naprawdę potrzeba. Zanim przejdziesz dalej, ustal swoją odpowiedź.
Cztery. Dwie liczby mówią, gdzie kwadrat się zaczyna. Dwie kolejne mówią, jaki jest duży. Odsłoń je na rysunku jedną po drugiej i sprawdź, czy Twoja obstawiona odpowiedź się broni. A jeśli obstawione było „dwie", to zgadza się połowa. Dwie liczby wystarczą na położenie, ale kwadrat bez rozmiaru nie wie, jak duży ma być.
Obie liczby położenia mierzy się od jednego punktu: od zera. Odsłoń strzałki na rysunku, jedną po drugiej.
Odległość w poziomie nazywa się x i rośnie w prawo. To nikogo nie dziwi. Odległość w pionie nazywa się y i rośnie w dół. To jest jedyna naprawdę przewrotna rzecz w całej arytmetyce tego kursu, więc nazwijmy ją wprost: im większy y, tym niżej na planszy. Pomyśl o kartce z tekstem. Pierwszą linijkę czytasz na samej górze, a każda następna leży niżej. Im dalszy numer linijki, tym niżej na stronie. Ekran liczy dokładnie tak samo, od góry w dół.
Kwadrat będzie biały, więc najpierw kolor. Wszystkie trzy światła idą na pełną jasność. Dopisz stałą pod kolorem tła:
KOLOR_TLO = (15, 25, 15)
KOLOR_ZNACZNIK = (255, 255, 255)
A teraz pora na samo rysowanie. To jedna linia w fazie RYSOWANIE, pod fill, bo najpierw maluje się tło, a dopiero na nim to, co ma być na wierzchu:
# ─── RYSOWANIE ───────────────────────────────
ekran.fill(KOLOR_TLO)
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (0, 0, 20, 20))
pygame.display.flip()
Przeczytaj nową linię od lewej, jak ścieżkę: z pakietu pygame weź dział draw (po angielsku „rysowanie"), a z niego funkcję rect, skrót od angielskiego słowa oznaczającego prostokąt. W nawiasie podajesz jej trzy rzeczy: na czym rysować (na Twoim ekran), jakim kolorem i wreszcie co dokładnie. To ostatnie to cztery liczby z kroku o czterech, w jednym nawiasie, zawsze w tej samej kolejności: x, y, szerokość, wysokość. Zapis (0, 0, 20, 20) znaczy więc: zacznij w punkcie zero-zero i narysuj 20 na 20 pikseli. Uruchom. W lewym górnym rogu planszy jest biały kwadrat, ten sam, który na początku lekcji był tylko obrazkiem.
Postaw drugi znacznik, w prawym górnym rogu. Róg jest górny, więc y zostaje zerem. Ale jaki x? Pierwszy odruch podpowiada 600, bo plansza ma 600 pikseli szerokości. Policz ze mną, dlaczego to za dużo. Cztery liczby opisują kwadrat od jego lewego górnego narożnika, więc kwadrat postawiony na x równym 600 zaczynałby się dokładnie tam, gdzie plansza się kończy, i cały wylądowałby poza nią. Żeby przylgnął do prawej krawędzi, musi się zacząć 20 pikseli przed nią: 600 - 20, czyli 580. Dopisz go pod pierwszym:
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (580, 0, 20, 20))
Dolne rogi to ten sam rachunek, tylko przeniesiony na oś pionową. To zarazem sprawdzian tego, co mówiła strzałka z kroku o zerze: większy y znaczy niżej. Dół planszy to y duże, i to dokładnie tak duże jak przy prawej krawędzi: 600 - 20, czyli 580. Lewy dolny róg dostaje więc x z powrotem równe zeru i y równe 580, a prawy dolny dostaje obie liczby po 580:
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (0, 580, 20, 20))
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (580, 580, 20, 20))
W oknie gry mają się pojawić cztery białe kwadraty, po jednym w każdym rogu ciemnozielonej planszy, każdy dokładnie tam, gdzie wysłały go Twoje liczby. Sprawdź wszystkie cztery rogi. I powiedzmy sobie od razu jasno: te znaczniki nie są elementami gry. To sonda. Postawiliśmy je po to, żeby zobaczyć na własne oczy, jak działa adresowanie, i sprawdzić rachunki na żywym ekranie. Na następnej lekcji znikną z pliku, bo zrobiły swoje, a ich miejsce zajmie pierwszy prawdziwy element gry. Uruchom.
Jest jedna pomyłka, którą przy prostokątach robi prędzej czy później każdy: zamiana x z y miejscami. Powiedzmy, że chcesz kwadrat w lewym dolnym rogu, a piszesz (580, 0, 20, 20) zamiast (0, 580, 20, 20). Kwadrat ląduje w prawym górnym. Program nie zgłasza przy tym żadnego błędu i działa dalej, jakby nigdy nic. Bo dla programu obie wersje są w porządku: 580 i 0 to dwie poprawne liczby, a on nie wie, która miała być odległością od lewej, a która od góry. Zapamiętaj więc objaw: kwadrat jest w innym miejscu, niż się spodziewasz, a błędu nigdzie nie widać. Gdy to zobaczysz, nie szukaj literówki. Policz jeszcze raz cztery liczby i sprawdź ich kolejność: najpierw od lewej, potem od góry.
Brama praktyki. Dostajesz miejsca na planszy opisane słowami, a Twoim zadaniem jest podać cztery liczby, które stawiają tam prostokąt. To dokładnie te rachunki, które w tej lekcji robiliśmy razem, tym razem w Twoich rękach. Wszystkie liczby są do policzenia w głowie.
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
FPS = 8
KOLOR_TLO = (15, 25, 15)
KOLOR_ZNACZNIK = (255, 255, 255)
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
# ─── RYSOWANIE ───────────────────────────────
ekran.fill(KOLOR_TLO)
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (0, 0, 20, 20))
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (580, 0, 20, 20))
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (0, 580, 20, 20))
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (580, 580, 20, 20))
pygame.display.flip()
zegar.tick(FPS)
To umiesz po tej lekcji: wiesz, że zero planszy leży w lewym górnym rogu i że y rośnie w dół; rysujesz prostokąt czterema liczbami i zanim uruchomisz program, potrafisz powiedzieć, gdzie wyląduje; rozpoznajesz po objawie zamianę x z y. Twój plik wygląda teraz tak:
import pygame
pygame.init()
SZEROKOSC = 600
WYSOKOSC = 600
FPS = 8
KOLOR_TLO = (15, 25, 15)
KOLOR_ZNACZNIK = (255, 255, 255)
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
# ─── RYSOWANIE ───────────────────────────────
ekran.fill(KOLOR_TLO)
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (0, 0, 20, 20))
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (580, 0, 20, 20))
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (0, 580, 20, 20))
pygame.draw.rect(ekran, KOLOR_ZNACZNIK, (580, 580, 20, 20))
pygame.display.flip()
zegar.tick(FPS)
Na następnej lekcji plansza przestanie być liczona w pikselach, a zacznie być liczona w kratkach. Na jej środku pojawi się głowa Twojego węża.
Plansza ma 600 pikseli szerokości. Gra podzieli ją na kratki, każdą o boku 20 pikseli. Tyle samo miały znaczniki z poprzedniej lekcji i to nie jest przypadek. Zanim przejdziesz dalej, policz dwie rzeczy. Pierwsza: ile takich kratek zmieści się w jednym rzędzie, od lewej krawędzi do prawej? Druga: ile kratek pokryje całą planszę? Oba rachunki są do zrobienia w głowie. Oba warto zrobić naprawdę, a nie tylko przeczytać, bo na tym przeliczeniu opiera się cała reszta tego kursu.
600 : 20 = 30, czyli w jednym rzędzie mieści się trzydzieści kratek. Rzędów jest tyle samo, bo plansza jest kwadratowa, a 30 × 30 = 900, czyli cała plansza to dziewięćset pól. Jeśli Twoje liczby się zgadzają, masz w ręku nową jednostkę tej gry: pole. Ekran dalej będzie mierzył wszystko w pikselach, ale wąż będzie się poruszał po polach, nie po pikselach. Jeden ruch to skok o całe pole, nigdy o pół czy ćwierć. Dziewięćset pól to cały świat tej gry.
Od tej chwili plansza ma dwie miary naraz. Odsłaniaj rysunek po jednej mierze.
Pola numerujemy od zera, dokładnie tak jak elementy list w Podstawach. Weźmy pole numer 3: gdzie ono się zaczyna na ekranie? Przed nim są trzy pola, zerowe, pierwsze i drugie, a każde jest szerokie na 20 pikseli. Trzy pola po 20 pikseli to 60, więc pole numer 3 zaczyna się w pikselu 60: 3 × 20 = 60. Mnożysz numer pola przez dwadzieścia i masz piksel. Teraz sprawdź się w ćwiczeniu poniżej, w którym policzysz kilka pól. Każdy rachunek jest do zrobienia w głowie i każdy warto zrobić naprawdę, bo to jest mnożenie, które Twoja gra będzie za kilka lekcji wykonywać bez przerwy.
A w drugą stronę? Znasz piksel i pytasz, w którym polu leży, powiedzmy piksel 340. Skoro z pola na piksel prowadzi mnożenie, z powrotem prowadzi dzielenie. Ale uwaga: numer pola musi być liczbą całkowitą, bo nie istnieje pole numer siedemnaście i pół. Dlatego dzielisz podwójną kreską: 340 // 20 = 17. To ten sam operator dzielenia całkowitego, którego używasz od Podstaw. Tutaj dostaje robotę, do której został stworzony. Sprawdź to rachunkiem z poprzedniego kroku: pole 3 zaczynało się w pikselu 60, a 60 // 20 to z powrotem 3. I jeszcze jedna próba, ciekawsza: piksel 347, wcale nie początek żadnego pola. 347 // 20 to nadal 17, bo podwójna kreska ucina resztę, więc każdy piksel wewnątrz pola wskazuje ten sam numer. Dokładnie tego chcemy: cokolwiek leży w polu siedemnastym, jest w polu siedemnastym.
Bok kratki będzie w tej grze wszędzie: w rysowaniu i w każdym rachunku, jaki gra będzie robić. A od lekcji o stałych wiesz, że liczba bez nazwy nic nie mówi. Dopisz więc do bloku stałych jedną linię, w której dwudziestka dostaje imię:
SZEROKOSC = 600
WYSOKOSC = 600
KRATKA = 20
FPS = 8
Nic nowego się tu nie dzieje. To dokładnie ta sama zasada, którą w lekcji o stałych zastosowaliśmy do sześćsetek i ósemki. Po prostu kolejna ważna liczba przestaje być gołą liczbą.
Zapowiedź z poprzedniej lekcji wchodzi w życie: cztery białe znaczniki zrobiły swoje i wypadają z pliku. Usuń z fazy RYSOWANIE wszystkie cztery linie z pygame.draw.rect, a z bloku stałych linię KOLOR_ZNACZNIK, bo nazwa, której nic już nie używa, tylko zaśmieca kod i myli czytającego. Plansza wraca do czystej, ciemnej zieleni. Jest pusta, i dobrze, bo za chwilę pojawi się na niej pierwszy prawdziwy element gry.
Pierwszy element gry to głowa węża: jeden zielony kwadrat. Ma się znaleźć na samym środku planszy, a środek policzysz oboma sposobami z tej lekcji. Połowa z 600 to 300, więc środek planszy to piksel 300. W którym polu leży piksel 300? Podwójna kreska: 300 // 20 = 15, czyli pole numer 15. A gdzie pole 15 zaczyna się na ekranie? Mnożenie: 15 × 20 = 300. Rachunek wrócił do punktu wyjścia i wszystko się spina: głowa znajdzie się w polu piętnastym, czyli od piksela 300, w poziomie i w pionie tak samo.
Do bloku stałych dochodzi kolor głowy, czyli zielone światło na pełnej jasności, żeby na prawie czarnym tle było ją widać od razu:
KOLOR_TLO = (15, 25, 15)
KOLOR_GLOWA = (80, 255, 100)
Pod zegar, jeszcze przed pętlą, trafiają dwie zmienne z położeniem głowy:
glowa_x = 300
glowa_y = 300
Zauważ małe litery. To nie są stałe, bo tej wartości nie obiecujemy trzymać bez zmian do końca programu, a tylko takie obietnice piszemy wielkimi literami. I wreszcie rysowanie, w fazie RYSOWANIE, pod fill:
ekran.fill(KOLOR_TLO)
pygame.draw.rect(ekran, KOLOR_GLOWA, (glowa_x, glowa_y, KRATKA - 2, KRATKA - 2))
Czytasz tę linię tak samo jak znaczniki z poprzedniej lekcji, tylko w miejscu gołych liczb położenia są teraz nazwy zmiennych. Jedno powinno Cię zatrzymać. W rozmiarze jest nie pełna KRATKA, tylko KRATKA - 2, czyli osiemnaście pikseli zamiast dwudziestu. Powód wyjaśnia następny krok. Uruchom. Na środku ciemnozielonej planszy jest mały zielony kwadrat. Nieruchomo, i na razie tak ma być.
Gdyby kwadrat głowy miał pełne 20 pikseli, wypełniałby swoje pole co do piksela. Brzmi porządnie. I właśnie dlatego, zanim się w to uwierzy, trzeba to zobaczyć. Porównaj oba obrazki.
Dwa kwadraty mniejsze o dwa piksele zostawiają między sobą cienką kreskę tła. Ta kreska robi całą robotę, bo dzięki niej oko widzi dwa osobne kwadraty, a nie jeden podłużny klocek. Po to jest KRATKA - 2. Za kilka lekcji, gdy wąż dostanie ciało złożone z wielu kwadratów, ta szczelina pozwoli Ci widzieć każdy segment osobno.
To najobszerniejsza brama praktyki w całym kursie, bo przeliczanie pól i pikseli to miejsce, w którym zacina się najwięcej osób, a jedynym lekarstwem jest przeliczyć samodzielnie, wiele razy, w obie strony. Ćwiczenia prowadzą z numeru pola na piksel przez mnożenie i z piksela na numer pola przez podwójną kreskę, z powtórką dzielenia całkowitego z Podstaw. Nie śpiesz się. Każdy rachunek jest do zrobienia w głowie, a każdy zrobiony naprawdę zostaje w niej na dobre.
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))
zegar = pygame.time.Clock()
glowa_x = 300
glowa_y = 300
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
# ─── 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)
To umiesz po tej lekcji: przeliczasz w obie strony, numer pola na piksel przez mnożenie i piksel na numer pola przez //; wiesz, że plansza to 900 pól po 30 w rzędzie; rozumiesz, po co przy rysowaniu odejmujemy dwa piksele. A na planszy jest pierwszy element gry. Twój plik wygląda teraz tak:
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))
zegar = pygame.time.Clock()
glowa_x = 300
glowa_y = 300
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
# ─── 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)
Zanim pójdziesz dalej, zobacz ten plik w prawdziwym oknie. Na samym środku planszy pojawi się zielony kwadrat. To jest głowa Twojego węża. Kliknij ▶ Uruchom w Kokpicie.
Głowa nie rusza się. Na następnej lekcji ruszy. Poznasz dwie liczby, które mówią grze, w którą stronę głowa ma skoczyć przy kolejnym obiegu pętli. I jedna umowa na przyszłość: glowa_x i glowa_y to rusztowanie pod jedną, samotną głowę. Gdy wąż dostanie ciało, ten zapis się zmieni, a lekcja, w której to nastąpi, powie Ci dokładnie, na co.
dx i dyTwoja plansza pracuje pełną parą. Osiem razy na sekundę pętla przemalowuje tło i rysuje zielony kwadrat, i osiem razy na sekundę rysuje go w tym samym miejscu. Nic w tym dziwnego: głowa jest na 300, 300, a faza LOGIKA, ta środkowa, jest wciąż pusta. Nikt nie każe jej się ruszyć.
Zaraz to zmienimy i głowa ruszy w prawo. Ale zanim dopiszesz choćby jedną linię, obstaw wynik. Plansza ma 600 pikseli szerokości, głowa startuje ze środka i jedzie w prawo. Co się stanie, kiedy dojedzie do prawej krawędzi: zatrzyma się na niej, odbije się jak piłka, czy pojedzie dalej i zniknie? Wybierz jedną odpowiedź i zapamiętaj ją. Sprawdzimy, gdy uruchomisz.
Na ekranie komputera nic się naprawdę nie przesuwa. Znasz notesy do kartkowania: na każdej kartce ta sama postać narysowana jest odrobinę dalej, a gdy szybko przerzucasz strony, oko widzi płynny ruch. Twoja pętla robi dokładnie to samo, bo każdy obieg to jedna kartka, malowana od zera.
Skoro tak, całe „poruszanie" sprowadza się do jednego pytania: gdzie narysować kwadrat na następnej kartce? A odpowiedź jest arytmetyką, którą liczysz w głowie. Odsłaniaj klatki po jednej i patrz na punkt. Żadna z tych pozycji nigdzie nie „jedzie". Program po prostu w każdym obiegu wylicza nową liczbę i tam stawia rysunek. Ruch to dodawanie i nic więcej.
Do pliku wchodzą dwie nowe zmienne. Trafiają nad pętlę, obok pozycji głowy:
dx = KRATKA
dy = 0
Nazwa dx to skrót, który czyta się „zmiana x", a dy czyta się „zmiana y". Razem mówią, o ile przesunąć głowę w każdym obiegu pętli: pierwsza liczba w poziomie, druga w pionie. Zero znaczy „w tej osi bez ruchu". Nasz zestaw, czyli KRATKA w poziomie i zero w pionie, opisuje więc jazdę w prawo, o jedno pole na obieg.
Jedno zdanie o tym, dlaczego dx = KRATKA, a nie dx = 20: wąż skacze o całe pole, więc długość skoku bierze się z rozmiaru kratki, nie z osobnej, przypadkowej liczby.
W lekcji o trzech fazach zostawiliśmy środek pętli pusty i zapowiedzieliśmy, że pewnego dnia pojawi się tam ruch. To jest ten dzień. Do fazy LOGIKA wchodzą dwie linie. Pierwszą, tę od poziomu, dostajesz gotową:
glowa_x += dx
Skrót += znasz z Podstaw: do pozycji głowy dokładamy przyrost. Drugą linię, tę od pionu, piszesz samodzielnie w ćwiczeniu poniżej, według tego samego wzorca. I to jest cała tajemnica ruchu w grach: co obieg LOGIKA wylicza nową pozycję, a RYSOWANIE tylko ją pokazuje. Gra najpierw myśli, potem maluje.
Za chwilę rozstrzygnie się Twój zakład o krawędź. Nie spuszczaj oka z kwadratu, zwłaszcza w chwili, gdy dojedzie do prawego brzegu planszy. Uruchom plik w Kokpicie.
Wyjeżdża za planszę i znika. Jeśli Twój typ brzmiał „pojedzie dalej", to trafiony. Ale powód jest ciekawszy niż sama odpowiedź. W całym Twoim pliku nie ma ani jednej linii, która mówiłaby, czym jest ściana. Gra niczego nie przegapiła. Ona po prostu nie wie, że plansza się kończy, bo nikt jej tego nie powiedział. Arytmetyka liczy dalej w najlepsze: 620, 640, 660... tylko że te pozycje leżą już poza oknem, więc rysunek ląduje tam, gdzie nikt go nie widzi.
To nie jest awaria, tylko zaplanowane odkrycie. Gra nauczy się, czym jest ściana, w lekcji o czujnikach śmierci. Na razie niech jeździ.
Czas spłacić dług. W lekcji o kolorach padło zdanie, że faza RYSOWANIE maluje całą klatkę od zera, i przyznaliśmy uczciwie, że różnicy jeszcze nie widać, bo na nieruchomej planszy nie ma jej prawa być. Obiecaliśmy dowód przy ruchu. Oto on.
Ten dowód zrobisz własnymi rękami, nie na obrazku. Postaw # na początku linii z ekran.fill(KOLOR_TLO). Linia zamieni się w notatkę, tak jak linie z nazwami faz, których program nie wykonuje. Uruchom i patrz na jadącą głowę.
Przez planszę ciągnie się gruba zielona smuga. Skąd ona? Ekran nie jest kartką, którą ktoś czyści co obieg. To tablica, na której farba zostaje, dopóki jej nie zamalujesz. Bez fill każdy dawny kwadrat trwa na swoim miejscu, a nowy tylko dochodzi obok. Po sekundzie masz ich osiem, po dwóch szesnaście. Usuń # i przywróć porządek.
Głowa umie na razie tylko w prawo, ale w parze dx i dy są już wszystkie cztery kierunki. Policzmy je na sucho, bez zmieniania pliku.
W prawo jedziesz przy dx = 20 i dy = 0, i to masz w pliku. W dół ruszysz przy dx = 0 i dy = 20, bo cały ruch po prostu przenosi się do drugiej osi. Pary dla lewa i dla góry policzysz samodzielnie w ćwiczeniu poniżej. W obu potrzebny będzie minus.
Minus wygląda podejrzanie tylko przez chwilę. Skoro „w dół" znaczy „więcej y", to „w górę" musi znaczyć „mniej y", dlatego droga w górę to odejmowanie. Nic nowego: to ta sama jedyna dziwna rzecz w arytmetyce tego kursu, tylko oglądana z drugiej strony.
Nie wpisuj jeszcze nic do pliku. Ten rachunek to zapas. Sięgniemy po niego szybciej, niż myślisz.
Dwie linie ruchu są krótkie i łatwo je postawić w złym miejscu. Najczęstszy błąd: lądują poza pętlą. Jeśli trafiły nad while, to zanim pętla w ogóle ruszy, wykonają się dokładnie raz, więc głowa pojawi się o jedno pole dalej i znieruchomieje. Jeśli trafiły pod całą pętlę, nie wykonają się nigdy, bo pętla się nie kończy, więc głowa nie ruszy się wcale.
Jest i trzecie złe miejsce, najbardziej podstępne: między rozkazami rysowania. Na oko wszystko może wtedy działać, ale ruch to decyzja gry, nie malowanie. Jego miejsce to faza LOGIKA, między ZDARZENIAMI a RYSOWANIEM. Kolejność faz z lekcji o pętli gry nie jest ozdobą; to mapa, dzięki której za kilka lekcji, przy dużo większym pliku, wciąż będzie wiadomo, co gdzie mieszka.
Zapamiętaj objawy: głowa skacze raz i zatrzymuje się, albo nie rusza się wcale. W obu przypadkach pierwsze pytanie brzmi „gdzie są moje dwie linie ruchu?".
Pora przełożyć to na palce. W ćwiczeniach poniżej policzysz, gdzie głowa znajdzie się po zadanej liczbie obiegów, dokładnie ten rachunek, który przed chwilą robiliśmy na sucho. Kalkulator nie będzie potrzebny.
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))
zegar = pygame.time.Clock()
glowa_x = 300
glowa_y = 300
dx = KRATKA
dy = 0
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
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)
To umiesz: ruch w grze to dodawanie dwóch liczb do pozycji w każdym obiegu pętli. dx mówi „o ile w poziomie", dy „o ile w pionie", a znak liczby wybiera stronę. Wiesz też, dlaczego tło maluje się od zera co obieg: bez tego po planszy ciągnęłaby się smuga dawnych klatek.
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))
zegar = pygame.time.Clock()
glowa_x = 300
glowa_y = 300
dx = KRATKA
dy = 0
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
# ─── LOGIKA ──────────────────────────────────
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)
Jedno trzeba powiedzieć głośno: głowa jedzie, ale tam, gdzie każe kod, bo dx i dy ustawiamy raz, nad pętlą, i nikt ich później nie zmienia. Na następnej lekcji sterowanie przejmie gracz: strzałki zaczną zmieniać dx i dy w trakcie jazdy.
Uruchom swój plik i spróbuj skręcić strzałką. Nic. Głowa jedzie w prawo, jakby klawiatura nie istniała. Z punktu widzenia gry rzeczywiście nie istnieje, bo w całym pliku nie ma ani jednej linii o klawiszach.
Oto blok, który dziś wejdzie do Twojego pliku. Zanim wyjaśnię choć jedno słowo, przeczytaj go jak zagadkę i obstaw: które słowo mówi „naciśnięto jakiś klawisz", a które nazywa jeden konkretny klawisz?
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_RIGHT:
dx, dy = KRATKA, 0
elif zdarzenie.key == pygame.K_LEFT:
dx, dy = -KRATKA, 0
elif zdarzenie.key == pygame.K_DOWN:
dx, dy = 0, KRATKA
elif zdarzenie.key == pygame.K_UP:
dx, dy = 0, -KRATKA
Wygląda gęsto, ale policz uczciwie: nowe są tu dokładnie trzy rzeczy. Cała reszta, czyli zdarzenie, if z elif i pary liczb z poprzedniej lekcji, to starzy znajomi. Rozbierzemy te trzy nowości po jednej.
Zacznijmy od dobrej wiadomości: mechanizm już znasz. W lekcji o zdarzeniach system odkładał każde zdarzenie do kolejki jak listonosz listy do skrzynki, a program wyjmował je pętlą for. Do tej pory wyjmowaliśmy stamtąd tylko jeden rodzaj przesyłki: wiadomość o klikniętym krzyżyku.
Naciśnięcie klawisza to po prostu drugi rodzaj zdarzenia, odkładany do tej samej kolejki, w tej samej kolejności, w jakiej się wydarzył. Odsłoń oba rodzaje na rysunku: ta sama taśma, dwie różne przesyłki. Nie uczysz się dziś nowego mechanizmu. Uczysz się rozpoznawać drugi rodzaj przesyłki w mechanizmie, który już masz.
Pierwsza z trzech nowości. Obok istniejącego warunku o krzyżyku, wewnątrz tej samej pętli zdarzeń, wchodzi drugi warunek:
if zdarzenie.type == pygame.KEYDOWN:
KEYDOWN to po angielsku „klawisz w dół", i ta nazwa jest precyzyjna. Zdarzeniem jest moment naciśnięcia, ta chwila, w której palec dociska klawisz, a nie jego trzymanie. Jedno naciśnięcie to jedna przesyłka w kolejce. Pytamy więc każde wyjęte zdarzenie po kolei: jesteś krzyżykiem? A może jesteś naciśniętym klawiszem?
Wiemy już, ŻE naciśnięto klawisz. Nie wiemy KTÓRY, a bez tego nie skręcimy. Tu wchodzi druga nowość: zdarzenie.key. Kropkę czytasz jak zawsze, od lewej, jako drogowskaz „szukaj w środku": to zdarzenie, a w nim jego klawisz. Słowo key to po angielsku właśnie „klawisz".
Trzecia nowość jest po drugiej stronie porównania. Pygame ma gotowe oznaczenie dla każdego klawisza na klawiaturze i wszystkie zaczynają się od K_, jak „klawisz": pygame.K_RIGHT to strzałka w prawo (po angielsku right, czyli prawo), K_LEFT w lewo, K_DOWN w dół, K_UP w górę. Porównanie zdarzenie.key == pygame.K_RIGHT czyta się więc jednym tchem: „czy naciśnięty klawisz to strzałka w prawo?".
Zanim podłączymy prawdziwe skręty, sprawdźmy samo słyszenie, jedną gałęzią i jednym print. Powiedzmy to sobie wprost: to jest rusztowanie na pięć minut. Stawiamy je, żeby zobaczyć, czy program w ogóle słyszy klawiaturę, i zanim skończy się ta lekcja, rozbierzemy je. W pliku po lekcji nie zostanie po nim ślad.
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_RIGHT:
print("prawo")
Uruchom i wciśnij strzałkę w prawo. Tylko nie patrz na planszę. Patrz pod nią, w Konsolę: tam trafia wszystko, co Twój program wypisuje przez print, tak samo jak od pierwszej lekcji Podstaw. Każde naciśnięcie dokłada linię „prawo". Na samej planszy nie zmienia się nic i tak ma być: podpięliśmy ucho, nie skutek. Głowa dalej jedzie w prawo swoim tempem, a pod planszą rośnie dowód, że gra słyszy.
Skoro wzorzec działa dla jednej strzałki, powielamy go na pozostałe. Żadnej nowej nazwy, czysta robota z elif. Gałąź dla lewa dostajesz gotową:
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_RIGHT:
print("prawo")
elif zdarzenie.key == pygame.K_LEFT:
print("lewo")
Dwie ostatnie gałęzie, dla dołu i dla góry, dopisujesz samodzielnie w ćwiczeniu poniżej, według tego samego wzorca. Potem uruchom i przewciskaj po kolei wszystkie cztery strzałki. W Konsoli rosną cztery różne napisy, więc gra nie tylko słyszy, ale i rozróżnia. Napisy są bez polskich ogonków, tak jak wszystkie nazwy w tym pliku; do tego, dlaczego tak, wrócimy jeszcze jednym zdaniem na następnej lekcji.
Sonda zrobiła swoje, więc rozbieramy rusztowanie. Wszystkie cztery print wychodzą z pliku, a w ich miejsce wchodzi prawdziwy skutek: zmiana kierunku jazdy. I tu nie ma nic do liczenia, bo cały rachunek zrobiliśmy na sucho na poprzedniej lekcji. Pierwszą gałąź dostajesz gotową, jako wzorzec wymiany:
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_RIGHT:
dx, dy = KRATKA, 0
Zapis dx, dy = KRATKA, 0 to przypisanie wielokrotne. Po lewej są dwie nazwy, po prawej dwie wartości, a Python najpierw liczy całą prawą stronę i dopiero potem rozdaje: pierwsza wartość trafia do pierwszej nazwy, druga do drugiej. Obie zmienne dostają nowe wartości naraz, więc nie da się ustawić pół kierunku. Jeśli masz za sobą piątą część Podstaw, tę oznaczoną M05, znasz ten zapis stamtąd. Jeśli nie, właśnie go poznajesz, a zanim użyjesz go w grze, rozgrzejesz go w ćwiczeniu niżej na prostym przykładzie.
Pozostałe trzy gałęzie przebudowujesz samodzielnie w ćwiczeniu poniżej, bo pary masz policzone z poprzedniej lekcji. A gdy skończysz, spójrz na swój blok: to jest dokładnie ten kod, który na początku lekcji był zagadką. Przeczytaj go teraz od góry do dołu. Nie ma w nim już ani jednego obcego słowa.
Za chwilę głowa ruszy w prawo. Kliknij w planszę, żeby złapała klawiaturę, i wciśnij strzałkę w dół. Ma skręcić. Potem w lewo, w górę, znowu w prawo: ma słuchać każdego ruchu. Od tej chwili to przestaje być animacja, na którą patrzysz, a zaczyna być gra, którą prowadzisz.
I zanim zamkniesz okno, jeden ruch specjalnie. Rozpędź głowę w prawo i wciśnij strzałkę w lewo. Głowa zawróci w miejscu, o sto osiemdziesiąt stopni. U Ciebie nie dzieje się przez to nic złego, ale w prawdziwym Snake'u zawrócenie w miejscu to ruch samobójczy, przegrana w ułamku sekundy. Zapamiętaj to zachowanie. Właśnie nim zajmie się następna lekcja. Uruchom.
Najczęstszy błąd tej lekcji wygląda niewinnie: pytanie o zdarzenie.key postawione poza sprawdzeniem rodzaju zdarzenia, na przykład prosto w pętli zdarzeń, bez warunku o KEYDOWN nad sobą. Program startuje normalnie i działa... dopóki nie ruszysz myszą. Wtedy wywala się czerwonym komunikatem, w którego ostatniej linii zobaczysz słowo key. Ten rodzaj lektury znasz już z lekcji o zdarzeniach: ostatnia linia mówi, czego zabrakło.
A zabrakło klawisza, bo nie każde zdarzenie go ma. Kolejką płyną przesyłki różnych rodzajów: ruch myszy to też zdarzenie, tylko że w środku nie ma żadnego klawisza. Pytanie „który klawisz?" wolno zadać dopiero wtedy, gdy rodzaj się zgadza. Dlatego warunek o key zawsze mieszka wewnątrz warunku o KEYDOWN. Objaw do zapamiętania: „działa, dopóki nie dotknę myszy" to niemal na pewno ta właśnie pomyłka.
Twoja kolej. W ćwiczeniach poniżej napiszesz z pamięci warunek, którym program rozpoznaje naciśnięty klawisz, bez podglądania w plik. Wszystko, czego potrzebujesz, przeszło Ci dziś przez ręce.
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))
zegar = pygame.time.Clock()
glowa_x = 300
glowa_y = 300
dx = KRATKA
dy = 0
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_RIGHT:
dx, dy = KRATKA, 0
elif zdarzenie.key == pygame.K_LEFT:
dx, dy = -KRATKA, 0
elif zdarzenie.key == pygame.K_DOWN:
dx, dy = 0, KRATKA
elif zdarzenie.key == pygame.K_UP:
dx, dy = 0, -KRATKA
# ─── LOGIKA ──────────────────────────────────
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)
To umiesz: rozpoznajesz w kolejce zdarzenie rodzaju KEYDOWN, wyjmujesz z niego zdarzenie.key i porównujesz z gotowymi oznaczeniami klawiszy z rodziny K_, a pod każdą strzałkę podpinasz zmianę pary dx, dy. Umiesz też postawić i rozebrać sondę z print, gdy chcesz sprawdzić, czy program w ogóle słyszy.
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))
zegar = pygame.time.Clock()
glowa_x = 300
glowa_y = 300
dx = KRATKA
dy = 0
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_RIGHT:
dx, dy = KRATKA, 0
elif zdarzenie.key == pygame.K_LEFT:
dx, dy = -KRATKA, 0
elif zdarzenie.key == pygame.K_DOWN:
dx, dy = 0, KRATKA
elif zdarzenie.key == pygame.K_UP:
dx, dy = 0, -KRATKA
# ─── LOGIKA ──────────────────────────────────
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)
Została jedna luka: gra słucha każdego rozkazu, nawet samobójczego zawrócenia w miejscu. Na następnej lekcji gra zacznie pamiętać, w którą stronę jedzie, i dzięki tej pamięci odmówi wykonania dokładnie tego jednego ruchu.
Na początek nic nie piszesz, tylko patrzysz. Kliknij przycisk „uruchom" i obejrzyj gotowego Snake'a, czyli ukończoną wersję gry, którą budujesz. Wąż ma już długie ciało i jedzie w prawo. W pewnej chwili gracz wciska strzałkę w lewo, i gra kończy się w ułamku sekundy. Głowa zawróciła w miejscu, prosto we własne ciało.
Zanim przejdziesz dalej, dwa pytania. Co dokładnie poszło nie tak? I ważniejsze: jak gra powinna się zachować, gdy gracz wciśnie taki klawisz?
Postawmy diagnozę na Twoim pliku. dx i dy mówią, o ile się przesunąć, ale nigdzie nie jest zapisane słowami, dokąd wąż jedzie. Owszem, da się to wyczytać z liczb: dx równe KRATKA przy dy równym zero znaczy „w prawo". Tylko spróbuj zbudować na tym warunek. „Jeśli dx jest równe minus KRATKA i dy jest równe zero, to znaczy, że jedziemy w lewo" zadziała, ale czyta się fatalnie, a przy czterech kierunkach urośnie z tego gąszcz porównań, w którym łatwo o pomyłkę.
Zrobimy inaczej, i to jest świadomy wybór projektanta, nie konieczność. Gra dostanie osobną nazwę, pod którą sama zapisze, dokąd jedzie. Kod, który czyta się jak zdanie, wygrywa z kodem, który trzeba rozszyfrowywać.
Obok dx i dy trafia trzecia zmienna:
dx = KRATKA
dy = 0
kierunek = "prawo"
W środku jest zwykły napis w cudzysłowie, znany z Podstaw, bez polskich ogonków, czyli "gora" i "dol", tak jak wszystkie nazwy w tym pliku. W prozie mówimy normalnie „w górę" i „w dół"; ogonki gubi tylko kod.
Sama deklaracja to jednak połowa roboty. Pamięć, której nikt nie aktualizuje, kłamie. Gdyby kierunek raz ustawiony został na zawsze, gra do końca świata twierdziłaby, że jedzie w prawo. Dlatego każda z czterech gałęzi dostaje po jednej linii aktualizacji. Pierwszą dostajesz gotową:
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_RIGHT:
dx, dy = KRATKA, 0
kierunek = "prawo"
Trzy pozostałe etykiety dopisujesz samodzielnie w ćwiczeniu poniżej. Od tej chwili gra w każdej sekundzie wie, dokąd jedzie. Wystarczy zapytać o kierunek.
Zanim napiszemy bezpiecznik, ustalmy dokładnie, czego ma zabraniać. Odsłaniaj pary po jednej.
Zauważ regułę, która z tego wynika: w każdej chwili zakazany jest dokładnie jeden klawisz, ten przeciwny do obecnej jazdy. Dwa skręty w bok są zawsze legalne, a czwarty klawisz to kierunek, w którym i tak już jedziesz. Bezpiecznik będzie miał więc bardzo mało roboty, i właśnie taki ma być.
andCałą tę tabelę załatwia jedno słowo, które znasz z Podstaw: and. Popatrz na pierwszą gałąź z dołożonym bezpiecznikiem:
if zdarzenie.key == pygame.K_RIGHT and kierunek != "lewo":
Przeczytaj na głos: „jeśli wciśnięto strzałkę w prawo i nie jedziemy właśnie w lewo". Operator jest stary, nowa jest jego rola. Dotąd warunki mówiły, kiedy coś zrobić; ten dokłada do zgody drugi wymóg i przez to mówi, kiedy czegoś nie robić.
Zatrzymaj się przy jednym wyborze, bo łatwo go przeoczyć: napisaliśmy kierunek != "lewo", choć kusząco wyglądałoby kierunek == "prawo". Czy to jest to samo? Zanim odpowiesz, policz, z ilu kierunków jazdy wolno skręcić w prawo. Rozstrzygniesz to w ćwiczeniu poniżej.
Taki warunek nazwiemy bezpiecznikiem, jak ten w domowej skrzynce z prądem. Przez całe miesiące nie robi nic i nikt o nim nie myśli. Czeka na jedną jedyną sytuację, w której coś mogłoby się spalić, i tylko wtedy przerywa obwód. Nasz przerywa dokładnie jeden ruch: samobójczy.
Wzorzec się nie zmienia, powielamy go na pozostałe gałęzie, po jednym and w każdej. Dokładasz je samodzielnie w ćwiczeniu poniżej; tabela czterech par z poprzedniego kroku mówi, która gałąź pyta o co. A gdy skończysz, zanim uruchomisz, sprawdź swój blok czytaniem, gałąź po gałęzi: strzałka w prawo pyta, czy nie jedziemy w lewo; strzałka w dół pyta, czy nie jedziemy w górę. Każda gałąź pilnuje swojej pary.
Zanim uruchomisz, jedno zdanie uczciwości: u Ciebie ten bezpiecznik jeszcze nic nie ratuje, bo Twój wąż nie ma ogona i zawrócenie niczego by dziś nie zepsuło. Zakładamy go teraz, żeby nie dokładać go później do gotowego sterowania. Sterowanie kończysz dziś i ono zostaje w tym pliku na zawsze.
Sprawdź bezpiecznik palcami. Rozpędź głowę w prawo i wciśnij strzałkę w lewo. Nie stanie się nic, i to jest dokładnie ta cisza, o którą nam chodziło. Bezpiecznik nie wyświetla ostrzeżenia i nie zatrzymuje gry; po prostu nie wpuszcza jednego ruchu, a wszystkie pozostałe przechodzą jak dawniej. Sprawdź i to: jadąc w prawo, skręć w dół albo w górę. Ma działać bez oporu. Uruchom.
Najczęstszy błąd tej lekcji to zapomniana aktualizacja: kierunek nie zostaje ustawiony w jednej z czterech gałęzi. Objaw jest osobliwy: trzy kierunki działają normalnie, a czwarty zachowuje się dziwnie. Konkretnie: jeśli w gałęzi „w dół" zabraknie linii kierunek = "dol", to po skręcie w dół gra dalej pamięta „prawo", i odmówi Ci skrętu w lewo, choć z jazdy w dół w lewo skręcić wolno.
Najcenniejsze w tym objawie jest to, na co wskazuje. Skoro trzy kierunki działają, błąd nie tkwi w rozpoznawaniu klawiszy, bo ta warstwa obsługuje wszystkie cztery identycznie. Błąd tkwi piętro wyżej, w pamięci gry. Tak właśnie szuka się błędów naprawdę: zanim przeczytasz choćby jedną linię kodu, objaw mówi, w której warstwie kopać.
Teraz Ty. W ćwiczeniach poniżej dołożysz bezpiecznik do wskazanej gałęzi i przy okazji odświeżysz dwie rzeczy, na których on się opiera: operator and z Podstaw i pary dx, dy z lekcji o ruchu.
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))
zegar = pygame.time.Clock()
glowa_x = 300
glowa_y = 300
dx = KRATKA
dy = 0
kierunek = "prawo"
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_RIGHT and kierunek != "lewo":
dx, dy = KRATKA, 0
kierunek = "prawo"
elif zdarzenie.key == pygame.K_LEFT and kierunek != "prawo":
dx, dy = -KRATKA, 0
kierunek = "lewo"
elif zdarzenie.key == pygame.K_DOWN and kierunek != "gora":
dx, dy = 0, KRATKA
kierunek = "dol"
elif zdarzenie.key == pygame.K_UP and kierunek != "dol":
dx, dy = 0, -KRATKA
kierunek = "gora"
# ─── LOGIKA ──────────────────────────────────
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)
To umiesz. I to jest kamień milowy: pełne sterowanie. Gra słucha klawiszy, pamięta pod jedną nazwą, dokąd jedzie, i odmawia wykonania jedynego ruchu, który w Snake'u kończy grę. Wszystko, co dzieje się między klawiaturą gracza a ruchem głowy, jest już w Twoim pliku i w tej postaci zostanie do końca kursu.
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))
zegar = pygame.time.Clock()
glowa_x = 300
glowa_y = 300
dx = KRATKA
dy = 0
kierunek = "prawo"
while True:
# ─── ZDARZENIA ───────────────────────────────
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
pygame.quit()
raise SystemExit
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_RIGHT and kierunek != "lewo":
dx, dy = KRATKA, 0
kierunek = "prawo"
elif zdarzenie.key == pygame.K_LEFT and kierunek != "prawo":
dx, dy = -KRATKA, 0
kierunek = "lewo"
elif zdarzenie.key == pygame.K_DOWN and kierunek != "gora":
dx, dy = 0, KRATKA
kierunek = "dol"
elif zdarzenie.key == pygame.K_UP and kierunek != "dol":
dx, dy = 0, -KRATKA
kierunek = "gora"
# ─── LOGIKA ──────────────────────────────────
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)
Na następnej lekcji weźmiesz na warsztat sposób zapisywania pozycji, taki, dzięki któremu wąż będzie mógł mieć więcej niż jeden kwadrat.