25 lekcji — od pustego okna do pełnej gry: własny statek, asteroidy rozpadające się na mniejsze, punkty, życia i ekran końca. Każda lekcja to działający program.
Czarne niebo, na nim gwiazdy. Pośrodku statek, który leci tam, gdzie go poprowadzisz, płynnie, sześćdziesiąt klatek na sekundę. Z góry nadlatują asteroidy. Strzelasz. Trafiona duża asteroida nie znika, tylko rozpada się na dwie średnie, a każda średnia na dwie małe. Za każdą są punkty. Im więcej punktów, tym szybciej lecą następne. Masz kilka żyć, a gdy skończą się wszystkie, gra pokazuje ekran końca i czeka na klawisz, którym zaczynasz od nowa.
Strzał, wybuch i pozostałe dźwięki nie są ściągnięte z sieci. Cztery dźwięki i trzynaście obrazków w tej grze powstają z kodu, który napiszesz. Cała gra to 267 linii w 23 funkcjach. Nie jest to dużo, ale każda z tych linii będzie Twoja, bo nie dostaniesz jej w gotowym pliku. Napiszesz ją.
Statek w tej grze nie jest obrazkiem, który ktoś narysował za Ciebie. Wyobraź sobie kartkę w kratkę i wzór, który powstaje przez zamalowanie wybranych pól: z daleka widać kształt, z bliska pojedyncze kwadraty. Tak samo powstaje ten statek. Zapisujesz go jako szesnaście linijek kropek i cyfr, gdzie kropka to puste pole, a cyfra to kolor. Zmienisz cyfrę, zmieni się barwa. Przesuniesz kilka znaków, zmieni się kształt.
To znaczy, że dwie osoby, które przejdą ten kurs, skończą z dwiema różnymi grami. Twój statek może być wąski i szybki w wyglądzie albo szeroki i ciężki, w jednym kolorze albo w trzech. Kiedy pokażesz grę komuś, pierwsze, co zobaczy, będzie rzeczą, którą zaprojektowałeś sam, znak po znaku.
Kurs ma 25 lekcji, trzy projekty i trzy egzaminy. W pierwszej lekcji zobaczysz coś, czego zwykle nie pokazuje się na początku: mapę dwudziestu trzech pustych pokoi. To nazwy wszystkich funkcji, z których zbudowana jest gotowa gra, każda z pustym wnętrzem. Przez cały kurs wypełniasz je po jednym. Nie ma tu momentu „a teraz przepiszmy to na funkcje", bo program ma swój kształt od pierwszej minuty, a Ty od pierwszej minuty piszesz kod tam, gdzie będzie mieszkał do końca.
Każda lekcja to kroki, które odsłaniasz przyciskiem. W każdym kroku coś robisz: uzupełniasz lukę, układasz klocki, odpowiadasz na pytanie, które otwiera dalszą drogę, albo piszesz kod w edytorze. Po każdej lekcji istnieje działający program, który uruchamiasz i oglądasz, nawet jeśli w pierwszej lekcji jest to tylko czarne okno.
Wszystko dzieje się w przeglądarce. Kod piszesz i uruchamiasz pod lekcją, niczego nie instalujesz i nie konfigurujesz. Droga „to samo u siebie na komputerze" istnieje, ale przychodzi na końcu kursu, gdy gra już działa.
Jeśli robiłeś naszego Snake'a, to jest ta różnica: tam kod rósł w dół jednym ciągiem i funkcje przyszły późno, tutaj plan całej gry masz przed sobą od pierwszej lekcji.
Ten kurs jest w budowie. Dziś gotowe są cztery pierwsze lekcje: od pustego okna do tła pomalowanego Twoją pierwszą funkcją. Reszta powstaje i będzie dochodzić lekcja po lekcji. Oglądasz więc kurs w trakcie powstawania, z jego szkieletem na wierzchu. Niektóre rzeczy mogą się jeszcze zmienić.
Co do tempa: jedna lekcja dziennie to dobry rytm, dwie to dobry rytm dla kogoś, kto ma więcej czasu. Ważniejsze od liczby jest to, żeby wracać regularnie, bo każda lekcja zaczyna się tam, gdzie skończyła poprzednia, i najłatwiej ją podjąć, gdy poprzednia jest jeszcze świeża. Lekcja, którą przerwiesz w połowie, poczeka na Ciebie w tym samym kroku.
Pierwsza lekcja zaczyna się od mapy.
Po lewej masz kadr z gotowej gry. Po prawej plik, z którego ta gra powstaje. Są w nim dwadzieścia trzy nazwy, każda zaczyna się od def, a pod każdą jest pusto. Na samym dole jest jedna linia bez def: main(). Tak wygląda plan tej gry. Wszystko, co zobaczysz w oknie przez następne tygodnie, będzie wypełnianiem tych pustych miejsc, po jednym na lekcję.
Zanim cokolwiek napiszesz, przyjrzyj się temu plikowi jak mapie. Z Podstaw wiesz, co robi def. Zapisuje przepis, ale go nie wykonuje. Przeczytaj więc cały plik od góry do dołu i odpowiedz na pytanie poniżej: od której linii Python zaczyna coś robić, a nie tylko zapamiętywać?
Jeśli masz za sobą Snake'a, ten plik wygląda znajomo, z jedną różnicą. Tam kod rósł w dół jednym ciągiem, a tutaj od pierwszej minuty ma kształt.
Właśnie dlatego program zaczyna się od dołu: góra to same przepisy, a dopiero ostatnia linia każe pierwszy z nich wykonać.
W Kokpicie pod tym krokiem leży gotowy plik z tej lekcji. Nie musisz go jeszcze rozumieć, wystarczy, że go uruchomisz. Tak wygląda w całości:
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
gramy = True
while gramy:
pygame.display.flip()
main()
Zanim klikniesz, zapamiętaj kolejność. Pojawi się okno. Spróbuj je zamknąć krzyżykiem, raz i drugi, i patrz, co się dzieje. Potem zatrzymaj program przyciskiem STOP w Kokpicie. Kliknij przycisk ▶ Uruchom, spróbuj zamknąć okno krzyżykiem, zatrzymaj program przyciskiem STOP, a potem kliknij przycisk Zwiń na pasku Kokpitu.
Pojawiło się okno. Czarne, osiemset na sześćset pikseli, i nic w nim nie było. Krzyżyk nie zrobił nic, za pierwszym razem ani za drugim. To okno nie umie się zamknąć; jedyne, co je zatrzymuje, to przycisk STOP w Kokpicie. Zapamiętaj ten objaw, bo to nie jest usterka Kokpitu ani przypadek. Ten plik ma dokładnie taką wadę, jaką ma mieć po pierwszej lekcji, i zanim ją naprawisz, dowiesz się, skąd się bierze. Tekst wrócił, więc czytaj dalej i przejdź do następnego kroku przyciskiem Dalej.
Pierwsza linia pliku to import pygame. Tą linią przynosisz do warsztatu pudełko z przegródkami i narzędziami w środku. Żeby czegoś użyć, mówisz, gdzie to leży, a w Pythonie mówi się to kropką i nawiasem: kropka to „wejdź głębiej", nawias to „użyj". Tyle zasady wystarczy Ci na cały kurs.
pygame.init() wchodzi do pudełka i naciska włącznik. Robi to raz, na początku, zanim sięgniesz po cokolwiek innego.
Druga linia w main ma więcej kropek. Prześledź ją po obrazku i policz, ile pudełek Python otwiera po drodze, zanim dojdzie do narzędzia.
set_mode otwiera okno i oddaje Ci je do ręki. Dlatego po lewej stronie jest ekran =. Od tej chwili masz w zmiennej rzecz, której będziesz wydawać polecenia przez cały kurs.
Podwójny nawias nie jest błędem. Zewnętrzny to nawias wywołania, ten sam co w init(); wewnętrzny to para liczb zapisana razem, jak krotka z Podstaw. set_mode dostaje więc jedną rzecz: parę „szerokość i wysokość".
Samych liczb nie ma w tej linii. Są na górze pliku, wielkimi literami, tak jak w Podstawach zapisuje się stałe, których nie zamierzasz zmieniać w trakcie programu. Policz poniżej, ile pikseli ma takie okno, i wpisz nazwy obu stałych tam, gdzie ich brakuje.
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
Program w Pythonie kończy się, gdy dojdzie do ostatniej linii. Gdyby main kończyła się na otwarciu okna, okno mignęłoby i zniknęło, bo nie miałby go kto trzymać. Trzyma je pętla.
while gramy: kręci się tak długo, jak długo w gramy jest True. Każdy obrót tej pętli to jeden obieg. Program nie czeka w niej na Ciebie. Kręci się bez przerwy, obieg za obiegiem, a okno zostaje, bo main wciąż trwa.
Wróć teraz do objawu z kroku 2. Okno nie dało się zamknąć krzyżykiem. Przeczytaj pętlę jeszcze raz, linia po linii, i odpowiedz: co w tym pliku może zmienić gramy na False?
Nic. W tym pliku nie ma linii, która w ogóle dotyka gramy po jej ustawieniu, więc warunek pętli nigdy nie przestaje być prawdą, a okno stoi, aż zatrzymasz je z zewnątrz. Linia, która to zmieni, przyjdzie w następnej lekcji.
gramy = True
while gramy:
pygame.display.flip()
flip — oddaj klatkęWewnątrz pętli jest na razie jedna linia i kończy się słowem flip. Gra rysuje obraz najpierw na zapleczu, poza oknem, a flip oddaje gotowy obraz do okna, żeby było go widać. Robi to w każdym obiegu, więc każdy obieg to jedna klatka: jeden obraz oddany do okna.
W tej lekcji nie ma jeszcze czego rysować, więc każda klatka jest pusta i okno zostaje czarne. To się zmieni, gdy w pętli pojawi się malowanie. Uzupełnij poniżej ścieżkę do flip; prowadzi przez tę samą przegródkę, co set_mode.
mainTeraz Ty. W edytorze poniżej jest szkielet main z pięcioma lukami. Wypełnij je tak, żeby powstał plik z kroku 2: włącznik, otwarcie okna, pętla i oddanie klatki. Kolejność jest taka sama jak w poprzednich krokach.
Zanim klikniesz, zapamiętaj, co zobaczysz. Jeśli wszystko jest na miejscu, pojawi się czarne okno osiemset na sześćset. Będzie tak samo głuche jak w kroku 2. Krzyżyk nic nie zrobi, bo w pliku wciąż nie ma linii, która zmienia gramy. To jest poprawne. Kliknij przycisk ▶ Uruchom, zatrzymaj program przyciskiem STOP, a potem kliknij przycisk Zwiń na pasku Kokpitu i wróć tutaj po werdykt.
Czarne okno osiemset na sześćset i głuchy krzyżyk: dokładnie tak ma być, bo w pliku nie ma jeszcze linii, która zmienia gramy. Program kończy się przyciskiem STOP, a werdykt i Dalej czekają pod krokiem.
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
gramy = True
while gramy:
pygame.display.flip()
main()
Spójrz na mapę jeszcze raz. Dwadzieścia trzy nazwy, a przy jednej z nich, przy main, jest już haczyk. Wypełnione: 1 / 23. To mało, ale to jedyna funkcja, bez której nie ruszy żadna inna. Włącza pudełko, otwiera okno i kręci pętlą, w której zmieści się cała reszta.
Twój plik ma dziesięć niepustych linii i jedną wadę: okna nie da się zamknąć. Następna lekcja dopisuje do pętli cztery linie. Po nich krzyżyk zacznie działać, program skończy się sam, a w Kokpicie zamiast przycisku STOP zobaczysz komunikat końca.
Klikasz krzyżyk, system pisze karteczkę, karteczka czeka w skrzynce obok pętli, a pętla czyta ją dopiero w najbliższym obiegu.
Każda taka karteczka to zdarzenie. Kliknięcie krzyżyka jest zdarzeniem, naciśnięcie klawisza jest zdarzeniem, ruch myszy też. Program nie dowiaduje się o nich w chwili, gdy się dzieją. Dowiaduje się wtedy, gdy zajrzy do skrzynki, a zagląda do niej w swoim tempie, raz na obieg.
Odpowiedz teraz na pytanie: co się dzieje z kliknięciem, które przychodzi, gdy pętla jest akurat zajęta czymś innym, na przykład oddawaniem klatki?
W Kokpicie leży plik po tej lekcji. Od pliku z poprzedniej lekcji różnią go cztery linie. Nie szukaj ich jeszcze; w następnych krokach dojdziesz do każdej z osobna i będzie jasne, dlaczego każda jest tam, gdzie jest.
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
pygame.display.flip()
pygame.quit()
main()
Zanim klikniesz, zapamiętaj kolejność: gdy pojawi się okno, klikasz krzyżyk i czytasz, co pojawi się w Kokpicie. Kliknij przycisk ▶ Uruchom, a gdy pojawi się okno, kliknij krzyżyk, przeczytaj napis w Kokpicie i kliknij przycisk Zwiń na jego pasku.
Okno zniknęło. W Kokpicie pojawił się napis: „Program się zakończył". Zapamiętaj go, bo od dziś będziesz go widywać po każdej udanej rozgrywce. To komunikat końca, nie błąd. Program doszedł do ostatniej linii i wyszedł tak, jak wychodzi każdy program w Pythonie, tyle że po drodze zamknął okno. Tekst wrócił, więc czytaj dalej i przejdź do następnego kroku przyciskiem Dalej.
Pierwsza nowa linia to pętla for, którą znasz z Podstaw. Zwróć uwagę, po czym się kręci. Kręci się po pygame.event.get(). Ścieżka mówi, gdzie to leży: w pudełku pygame, w przegródce event. Nawias mówi „użyj".
get() robi coś, czego obrazek nie pokazał. Wyjmuje karteczki i opróżnia skrzynkę. Dostajesz listę wszystkiego, co przyszło od poprzedniego obiegu, a skrzynka zostaje pusta. Gdyby zawołać get() w tym samym obiegu drugi raz, wróciłaby pusta lista, bo nikt w międzyczasie nic nie wrzucił.
To ma znaczenie w obie strony. Gdy karteczek jest dużo, for przejdzie po wszystkich, po jednej na obrót. A gdy skrzynka jest pusta? Pomyśl, ile razy wykona się wtedy ciało tej pętli, i odpowiedz poniżej.
for zdarzenie in pygame.event.get():
QUIT i FalseKażda karteczka ma napis, który mówi, co się stało. W kodzie ten napis czyta się przez zdarzenie.type: bierzesz karteczkę, którą for właśnie trzyma, i patrzysz na jej nagłówek.
Z czym go porównać? Nie wpisujesz napisu z pamięci. W pudełku pygame leży gotowy wzór dla każdego rodzaju karteczki, a ten od krzyżyka nazywa się pygame.QUIT. Gdy nagłówek karteczki zgadza się ze wzorem, wiesz, że ktoś prosi o zamknięcie.
Co wtedy? Niczego nie zamykasz siłą. Wystarczy powiedzieć pętli, że ma przestać się kręcić, a z poprzedniej lekcji wiesz, co o tym decyduje. Uzupełnij linię wewnątrz if.
if zdarzenie.type == pygame.QUIT:
False pętla robi jeszcze jednoWpisujesz False do gramy. Co Python robi w tej samej chwili? Wielu ludzi odpowiada, że wychodzi z pętli. Sprawdź to, zamiast wierzyć.
Weź plik z kroku 2 i prześledź obieg, w którym w skrzynce leży karteczka QUIT. for ją wyjmuje, if ją rozpoznaje, gramy dostaje False. Teraz prowadź palcem. Która linia wykona się jako następna? Zanim przeczytasz dalej, odpowiedz poniżej.
Pętla nie wyskakuje. Dobiega do końca obiegu, wykonuje wszystko, co w nim zostało, i dopiero wracając na górę sprawdza warunek. Warunek jest fałszywy, więc pętla się kończy, a main idzie dalej, do linii pod pętlą. Z tego wynika, gdzie ma stać gaszenie Pygame: po pętli, w miejscu, do którego program dochodzi wtedy, gdy ostatni obieg naprawdę się skończył. To jest ostatnia z nowych linii: pygame.quit(), na końcu main.
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
pygame.display.flip()
pygame.quit()
Na obrazku są dwa pliki. Prawy jest tym z tej lekcji. W lewym ktoś nie wpisał False do gramy, tylko w środku if wpisał od razu pygame.quit(), bo skoro ktoś chce zamknąć, to gaszę. Brzmi logicznie. Pod lewym plikiem jest czerwony komunikat:
pygame.error: video system not initialized
Po polsku: system obrazu nie jest włączony. Ktoś zgasił Pygame, a zaraz potem jakaś linia poprosiła je jeszcze o coś. Z poprzedniego kroku wiesz, że po False obieg dobiega do końca. Po quit() obieg też dobiega do końca. Która linia zgłosiła ten błąd? Wskaż ją poniżej.
Ten nawyk ma nazwę: gaszenie w środku i ucieczka z pętli. Jeśli masz za sobą Snake'a, znasz go z własnego pliku. Tam wyjście pisało się pygame.quit() i raise SystemExit wewnątrz if i działało, bo pętla była na wierzchu pliku, poza wszystkimi def. Tutaj nie ma z czego uciekać. main kończy się sama, więc gaszenie ma swoje miejsce po pętli.
Masz klocki. Ułóż z nich w edytorze plik z kroku 2: co idzie do pętli, co pod nią, a co w ogóle do tego pliku nie pasuje.
Kliknij przycisk ▶ Uruchom, a gdy pojawi się okno, kliknij krzyżyk, przeczytaj, co mówi Kokpit, i kliknij przycisk Zwiń na jego pasku.
Okno zamknęło się, a program skończył się sam, bez przycisku STOP. Masz pętlę, która słucha i umie się skończyć. Kręci się tak szybko, jak tylko zdąży, a w następnej lekcji dostanie metronom.
Krzyżyk po raz pierwszy coś zrobił: zdarzenie QUIT trafiło do Twojego if, gramy zmieniło się na False i pętla się skończyła. Kokpit nie musiał niczego przerywać. Zwiń go i kliknij Dalej.
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
pygame.display.flip()
pygame.quit()
main()
Na ekranie są dwa okna i dwie liczby. W lewym figura skacze po kratkach, całą kratkę naraz, osiem razy na sekundę. W prawym figura płynie po pikselach, sześćdziesiąt razy na sekundę. Obie mają w ciągu sekundy pokonać tę samą drogę: dwieście czterdzieści pikseli.
Zanim dowiesz się, po co Ci te liczby, policz. O ile pikseli musi się przesunąć lewa figura przy każdym skoku, a o ile prawa przy każdym kroku, żeby obie zdążyły w sekundę?
Lewa skacze o całą kratkę i tak ma być: gra na kratkach może iść wolno, bo skok jest jej naturą. Prawa robi drobne kroki i musi ich robić dużo, bo każdy większy byłoby widać jako szarpnięcie. Gra na kratkach może iść wolno, gra na pikselach musi iść gładko. Ta gra jest z prawej strony, a liczba sześćdziesiąt trafi do Twojego pliku jeszcze w tej lekcji.
Żeby pętla trzymała tempo, potrzebuje czegoś, co mierzy czas. Pygame ma na to gotową rzecz i wydaje ją tak samo jak okno. pygame.time.Clock() to ścieżka do przegródki time i nawias, który mówi „zrób mi zegar". To, co wraca, zapisujesz pod nazwą zegar, tak jak wcześniej okno pod nazwą ekran.
Ta linia jest przed pętlą, obok otwarcia okna, a nie w niej. Zegar ma być jeden i ma pamiętać, kiedy skończył się poprzedni obieg. Gdyby powstawał w każdym obiegu od nowa, zaczynałby liczyć od zera i nie miałby z czym porównać. Rzeczy, które mają trwać przez całą grę, tworzysz raz, przed pętlą. Z oknem już tak było; z zegarem robisz to samo.
zegar = pygame.time.Clock()
Wyobraź sobie zespół muzyczny bez perkusisty. Każdy gra we własnym tempie i utwór się rozjeżdża. Dlatego muzycy ćwiczą z metronomem, urządzeniem, które tyka idealnie równo i wszystkim narzuca wspólne tempo.
Zegar z poprzedniego kroku umie być metronomem. Linia zegar.tick(60) na końcu obiegu mówi: sześćdziesiąt kresek na sekundę.
zegar.tick(60)
Odpowiedz teraz poniżej, co dokładnie robi tick, gdy obieg skończył się szybciej, niż przyszła kreska.
Po co? Bo bez tego pętla kręci się tak szybko, jak zdąży, a to zależy od komputera. Na szybkim zrobi tysiące obiegów na sekundę, na wolnym sto, i ta sama gra u dwóch osób szłaby w dwóch różnych tempach. Z metronomem obie idą w sześćdziesiąt kresek na sekundę.
Popatrz na dwie linie z tej lekcji. W pygame.time.Clock() kropka prowadzi w głąb pudełka, do przegródki, do narzędzia. To kropka, którą znasz od pierwszej lekcji, czyli ścieżka. W zegar.tick(60) kropka jest po rzeczy, którą już trzymasz w ręce, i mówi jej, co ma zrobić. To druga kropka: polecenie dla rzeczy.
Rozróżnisz je bez zastanowienia. Przed kropką ścieżki jest nazwa pudełka albo przegródki, coś, co przyszło z import. Przed kropką polecenia jest Twoja zmienna, coś, co wcześniej do Ciebie wróciło i dostało nazwę. ekran też jest taką rzeczą i też przyjmuje polecenia; pierwsze wydasz mu w następnej lekcji.
Znasz jeszcze zdarzenie.type. To trzeci przypadek: kropka po rzeczy, ale bez nawiasu, bo niczego jej nie każesz, tylko odczytujesz, co ma napisane. Wróci w swoim czasie. Poniżej rozdziel wyrażenia do dwóch koszy.
Każdy obieg pętli robi trzy rodzaje rzeczy i będzie tak do końca kursu. Maluje to, co ma być w oknie. Słucha, co przyszło do skrzynki. Liczy, co się zmieniło: gdzie co jest, co w co uderzyło, ile czego zostało. Trzy szuflady, i wszystko, co kiedykolwiek wpiszesz do pętli, trafi do jednej z nich.
W Twoim pliku dwie szuflady mają już zawartość, a jedna jest pusta. Szuflada LICZ jest pusta celowo: to miejsce na wszystko, co się rusza, a na razie nic się nie rusza. Zostanie pusta jeszcze przez kilka lekcji i wypełni się, gdy będzie co liczyć.
Szuflady mają stałą kolejność i nie bierze się ona z przyzwyczajenia. Bierze się z jednego pytania: co musi być gotowe, zanim namalujesz? Odpowiedz sobie na nie, a kolejność ułoży się sama. Ułóż trzy fazy poniżej.
Wróć do mapy z pierwszej lekcji. Dwadzieścia trzy nazwy, z których jedna jest już wypełniona. Teraz wiesz, że obieg ma trzy szuflady, i możesz zobaczyć, że mapa i szuflady to jedna rzecz: każde ciało z mapy należy do jednej szuflady, a main woła je stamtąd w kolejności obiegu. Ciało, które rysuje, mieszka w MALUJ. Ciało, które sprawdza klawisze, w SŁUCHAJ. Ciało, które przesuwa albo zderza, w LICZ.
Nazwy na mapie są po polsku nie z przypadku i nie dla ozdoby. Są po polsku po to, żeby z samej nazwy dało się zgadnąć, co ciało robi i do której szuflady należy, zanim jeszcze zobaczysz jego środek. Sprawdź to na sobie. Poniżej masz sześć nazw z mapy, rozłóż je do trzech szuflad.
W edytorze są dwie luki: jedna przed pętlą, jedna na jej końcu. Wypełnij je liniami z tej lekcji.
Uczciwie: w ramce nie zobaczysz różnicy. Okno będzie tak samo czarne jak w poprzedniej lekcji i tak samo zamknie się krzyżykiem, bo metronom wyrównuje tempo obiegów, a w obiegach nie ma jeszcze niczego, co by się ruszało. Różnicę zobaczysz, gdy statek ruszy. Z metronomem popłynie równo, a bez niego pomknąłby na jednym komputerze i wlókł się na drugim. Werdykt pod edytorem sprawdza Twój plik, nie Twoje oko. Kliknij przycisk ▶ Uruchom, a potem kliknij przycisk Zwiń na pasku Kokpitu i spójrz na werdykt.
Brak różnicy w oknie to wynik, nie błąd: metronom pilnuje tempa obiegów, a tempo widać dopiero na czymś, co się rusza. Ten dowód przyjdzie razem ze statkiem. Werdykt pod edytorem widzi więcej niż okno, więc zwiń Kokpit i kliknij Dalej.
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Twoja pętla ma metronom ustawiony na sześćdziesiąt kresek. Zanim sprawdzisz, obstaw: ile obiegów zrobi ta pętla w ciągu jednej sekundy?
W następnej lekcji do szuflady MALUJ wejdzie pierwszy pokój z mapy.
Ktoś przed Tobą doszedł do tego miejsca i napisał pierwszą funkcję poza main. Nazwał ją rysuj_tlo, dał jej pusty nawias, w środku kazał oknu pomalować się na kolor CZARNY i zawołał ją z pętli. Uruchomił i dostał na czerwono:
NameError: name 'ekran' is not defined
Nazwa ekran nie istnieje. Przecież istnieje, bo jest w pliku, w main, od pierwszej lekcji. Dlaczego Python jej nie widzi?
Odpowiedź znasz z Podstaw, trzeba ją tylko tu przyłożyć. Nazwa utworzona wewnątrz funkcji istnieje tylko w tej funkcji. ekran powstał w main, więc widzi go main i nikt inny. Nazwy zapisane na wierzchu pliku, poza wszystkimi def, widzi za to każda funkcja. Wierzch pliku widzą wszyscy; wnętrze main widzi tylko main. Spójrz na cudzy plik i wskaż poniżej, które z trzech nazw jego funkcja w ogóle widzi.
ekran wchodzi przez nawiasFunkcja nie widzi ekran, więc trzeba jej go dać. Z Podstaw wiesz, jak funkcja dostaje rzeczy z zewnątrz: przez nawias, jako parametr. Tutaj nazwiemy go po swojemu, ucho. Do pokoju rysuj_tlo dokładasz ucho, a main, wołając go, wkłada w to ucho okno. W środku pokoju ekran jest już zwykłą nazwą, którą widać.
Pusty nawias w cudzym pliku był całym błędem. Nie brakowało tam wiedzy o malowaniu, brakowało ucha.
Zapamiętaj ten ruch, bo to nie jest wyjątek na dziś. Każdy następny pokój z mapy, który ma cokolwiek zrobić z oknem, dostanie swoje ucho tak samo, a main tak samo będzie mu podawać ekran. Uzupełnij poniżej dwie luki: jedną w nagłówku pokoju, drugą tam, gdzie main go woła.
Jeśli masz za sobą Snake'a, tam ten błąd nie miał prawa się zdarzyć. ekran leżał na wierzchu pliku, a malowanie było wprost w pętli. Tutaj ekran mieszka w main, bo tak wygląda plik zbudowany z pokoi.
Kolor to trzy liczby, nie trzy kolory: ile czerwonego, ile zielonego i ile niebieskiego światła dokładasz, każde od 0 do 255. Wszystkie trzy na zero to brak światła, czyli czerń; wszystkie trzy na 255 to pełne światło, czyli biel.
Tło tej gry to (26, 28, 64). To nie czarna dziura, tylko noc, bardzo ciemna, z odrobiną granatu, taka, na której później będzie dobrze widać wszystko, co jasne. Dlatego stała nazywa się CZARNY, choć obok czystej czerni widać, że czarna co do joty nie jest.
Zapis trzema liczbami w tej kolejności nazywa się RGB, od angielskich nazw barw: red, green, blue. Uzupełnij poniżej brakującą liczbę w krotce tła.
ekran.fill — polecenie dla oknaWnętrze pokoju to jedna linia: ekran.fill(CZARNY). fill maluje całe okno jednym kolorem, od rogu do rogu, i zamalowuje wszystko, co było tam wcześniej. Dlatego jest pierwsze w malowaniu. Wszystko inne, co kiedykolwiek narysujesz w tej grze, wyląduje na tym tle, a każda klatka zacznie się od czystego okna.
Gdyby nie zamalowywało wszystkiego, w każdej klatce zostawałyby ślady poprzedniej. Na razie nie ma czego zostawiać, ale ten nawyk ma być gotowy, zanim pojawi się cokolwiek, co zmienia miejsce.
Spójrz jeszcze raz na tę linię i na to, co stoi przed kropką. Z poprzedniej lekcji znasz dwie kropki. Rozstrzygnij poniżej, która to.
def rysuj_tlo(ekran):
ekran.fill(CZARNY)
main zawołaćPokój jest gotowy, ale sam się nie wykona. Ktoś musi go zawołać, a woła zawsze main, z wnętrza pętli, w szufladzie MALUJ. Zasada z poprzedniej lekcji: najpierw maluj, potem pokaż. Malujesz na zapleczu, a flip oddaje gotowy obraz do okna, więc wywołanie rysuj_tlo(ekran) ma zdążyć przed flip; inaczej okno dostawałoby klatkę sprzed pomalowania.
Uczciwie: dziś zła kolejność jeszcze nie boli. Malujesz co obieg ten sam kolor, więc klatka spóźniona o jeden obieg wygląda dokładnie tak samo jak ta właściwa. Zaboli, gdy na tle pojawi się statek, bo wtedy „maluj, potem pokaż" zacznie decydować o tym, czy w ogóle go widać. Lepiej ustalić to miejsce teraz, gdy pomyłka jest bezkarna. Ułóż poniżej wywołanie we właściwym miejscu pętli.
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
rysuj_tlo(ekran)
pygame.display.flip()
zegar.tick(60)
Masz pełny edytor z plikiem po poprzedniej lekcji. Dopisz do niego trzy rzeczy: stałą CZARNY na wierzchu pliku, pokój rysuj_tlo z uchem i jego wywołanie w pętli, w miejscu ustalonym w poprzednim kroku. Pilnuj, żeby pokój był nad main, tak jak wszystkie na mapie.
Zanim klikniesz i pomyślisz, że nic się nie zmieniło, jedno słowo o oku. Gołym okiem nie odróżnisz nowego tła od czerni z poprzedniej lekcji, bo (26, 28, 64) jest tak ciemne, że dla oka to wciąż czerń. Różnicę widzi odczyt pikseli pod ramką i to on wystawia werdykt. Jeśli werdykt jest zielony, okno jest pomalowane Twoją własną funkcją, pierwszą w tym kursie i pierwszą, która dostała coś w nawiasie. Kliknij przycisk ▶ Uruchom, a potem kliknij przycisk Zwiń na pasku Kokpitu i spójrz na werdykt.
Co obieg pętla wchodzi do Twojego pokoju: rysuj_tlo dostaje ekran i maluje go kolorem (26, 28, 64). Że wygląda to jak czerń, to sprawa oka, nie kodu; odczyt pikseli widzi różnicę. Zwiń Kokpit i kliknij Dalej.
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
CZARNY = (26, 28, 64)
def rysuj_tlo(ekran):
ekran.fill(CZARNY)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
rysuj_tlo(ekran)
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Skoro oko nie widzi tła, zrób mu przysługę. Zmień w swoim pliku trzy liczby w CZARNY na coś jasnego, choćby (200, 220, 255). Tym razem różnicy nie da się przeoczyć. Całe okno będzie w jasnym kolorze, pomalowane tą samą funkcją, tylko z innymi liczbami w stałej.
Pobaw się chwilę. Sam czerwony na maksimum. Sam zielony. Wszystkie trzy na sto. Każde uruchomienie to inne tło, a kolor okna jest w Twoich rękach, w jednej linii na wierzchu pliku. Między jednym kolorem a drugim zwijasz Kokpit przyciskiem Zwiń na jego pasku, zmieniasz liczby i uruchamiasz jeszcze raz.
Gdy skończysz, wróć do wersji kursu: (26, 28, 64). Noc jest po coś. W następnej lekcji pojawi się na niej Twój własny statek, zbudowany z szesnastu linijek kropek i cyfr. Kliknij przycisk ▶ Uruchom i obejrzyj swoje tło, a gdy wrócisz do nocy, kliknij przycisk Zwiń na pasku Kokpitu i przejdź do następnej lekcji przyciskiem Dalej.
W Kokpicie leży plik po tej lekcji. Zanim klikniesz, wiedz, czego szukać. Na pierwszy rzut oka okno będzie wyglądać tak samo jak po poprzedniej lekcji. Granat i nic więcej. A jednak coś w nim jest. Gdzieś w tym oknie jest statek, szesnaście pikseli na szesnaście. Nie powiem, gdzie. Kliknij przycisk ▶ Uruchom, znajdź go, a gdy go znajdziesz, zatrzymaj program przyciskiem STOP i kliknij przycisk Zwiń na pasku Kokpitu.
Tyle daje szesnaście linijek tekstu i jedna funkcja. Że statek jest mały, widać gołym okiem; co z tym zrobić, zostawiam następnej lekcji. W tej dowiesz się, skąd on się wziął, i pod koniec zdecydujesz, jak ma wyglądać. Tekst wrócił, więc czytaj dalej i przejdź do następnego kroku przyciskiem Dalej.
Na wierzchu pliku, nad wszystkimi funkcjami, doszła lista o nazwie STATEK. To szesnaście napisów w cudzysłowach, jeden pod drugim, każdy po szesnaście znaków. Widzisz ją w panelu obok. Znaki są tylko dwojakie, kropka albo cyfra. Czytana z daleka, ta lista jest obrazkiem. Czytana z bliska, to zwykła lista napisów, taka sama jak listy z Podstaw, i tak samo się ją przechodzi: pętla po linijkach, a w każdej linijce pętla po znakach.
Zanim powiem, co znaczy kropka, a co cyfra, policz. Napisz funkcję policz_pelne, która dostaje taką listę i oddaje liczbę znaków, które nie są kropką. Sprawdź ją najpierw na małych listach, a potem na całym STATEK.
Sto trzydzieści siedem. Tyle znaków w siatce nie jest kropką i dokładnie tyle pikseli ma statek z kroku 1. Kropka to pole puste, cyfra to kolor. Pod siatką leży słownik KOLORY: sześć cyfr, a przy każdej trzy liczby, takie same trójki jak w CZARNY z poprzedniej lekcji. Funkcja, którą poznasz w tej lekcji, robi z listy obraz tak, jak Ty właśnie liczyłeś: linijka po linijce, znak po znaku, i przy każdej cyfrze maluje jeden piksel. Sto trzydzieści siedem razy.
Okno, które dostajesz z set_mode, jest kartką. Umie się pomalować i można na nim coś położyć. Pygame umie zrobić drugą kartkę, osobną od okna, dowolnej wielkości. Robi to pygame.Surface, po polsku „powierzchnia". Podwójny nawias znasz z set_mode: w środku para liczb, szerokość i wysokość. Tu obie wynikają z siatki, bo szerokosc to długość pierwszej linijki, a wysokosc to liczba linijek. Szesnaście na szesnaście.
Drugi argument, pygame.SRCALPHA, mówi, z czego ta kartka ma być zrobiona: z przezroczystej folii, nie z papieru. Tam, gdzie nic nie namalujesz, będzie widać to, co leży pod spodem.
Ktoś zbudował kartkę bez tego drugiego argumentu, pomalował statek i położył ją na granatowym tle. Co zobaczy wokół statku?
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
Kartka bez SRCALPHA rodzi się czarna, nie przezroczysta. Kropka to miejsce, którego funkcja nie maluje, więc zostaje na nim to, co było na kartce od początku: czerń. Statek dostaje wokół siebie czarny kwadrat szesnaście na szesnaście. Na granacie z poprzedniej lekcji ten kwadrat ledwo widać, bo czerń i granat są dla oka prawie tym samym. Zobaczysz go naprawdę za dwie lekcje, gdy pod statkiem staną gwiazdy: czarna kartka zasłoni każdą, która trafi pod statek.
Żeby pomalować piksel, funkcja musi najpierw wiedzieć, jaki znak jest w siatce w danym miejscu. Odczytuje go jedną linią:
znak = ksztalt[y][x]
Tę parę nawiasów znasz z Podstaw, z jedną różnicą. Tu w środku jest napis, więc drugi nawias wybiera znak, nie element listy.
Najpierw y, czyli który wiersz, potem x, czyli który znak w tym wierszu, oba liczone od zera. Zapamiętaj tę kolejność, bo za dwa kroki zobaczysz, co się dzieje, gdy się ją zgubi.
Odczytaj z siatki trzy znaki, o które pyta zadanie.
Trzy pierwsze linie funkcji już znasz: szerokość, wysokość, kartka. Reszta to dwie pętle i jedno malowanie.
def zbuduj_statek(ksztalt, kolory):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
obraz.set_at maluje jeden piksel kartki. Dostaje dwie rzeczy: gdzie, jako parę liczb, i na jaki kolor, jako trzy liczby. Kolor bierze się ze słownika. kolory[znak] tłumaczy cyfrę z siatki na trójkę liczb, dokładnie tak, jak słowniki tłumaczyły klucze na wartości w Podstawach.
Jest jeden haczyk. Kropka nie ma koloru, więc nie ma jej w słowniku. Zanim weźmiesz ze słownika kolor, zapytaj go, czy w ogóle zna ten znak: if znak in kolory. Kropki przechodzą przez to pytanie i nic się dla nich nie dzieje, a to jest cała przezroczystość statku.
Ułóż ciało funkcji z klocków: przejdź każdy wiersz, w nim każdy znak, pomaluj piksel, jeśli znak ma kolor, i na końcu oddaj kartkę. Jeden klocek jest zbędny. Jedno uczciwe zdanie na koniec. Zamiana range(szerokosc) z range(wysokosc) niczego tu nie zmieni, bo siatka jest kwadratowa. Zmieni, gdy Twoja przestanie być.
Obok masz dwa obrazy, oba powiększone. Lewy to nasz statek, dziobem w górę. Prawy powstał z tej samej siatki i tych samych kolorów, ma tyle samo pomalowanych pikseli, sto trzydzieści siedem, a leży na boku, dziobem w lewo. Program nie zgłosił żadnego błędu.
Ktoś w funkcji pomylił kolejność. Pod każdym z obrazów masz kod, który go zrobił. Która pomyłka położyła statek?
Obie. I to jest ciekawsze niż sama reguła. Siatka i kartka zadają dwa różne pytania. Siatka pyta „który wiersz?", a wiersz to pion, dlatego pierwszy nawias to y. Kartka pyta „gdzie na mnie?", a na kartce najpierw idzie się w poziomie, dlatego para w set_at zaczyna się od x. Czytasz [y][x], malujesz (x, y). Kolejność jest odwrotna nie ze złośliwości, tylko dlatego, że to dwa pytania. Jeśli odwrócisz którekolwiek z nich, wiersze zamienią się z kolumnami i obraz obróci się tak samo. Dwa miejsca, jeden obrót.
Funkcja oddaje kartkę ze statkiem. Kartka leży w zmiennej, ale sama z siebie nie jest w oknie. Żeby ją tam położyć, okno ma polecenie blit: przyklej tę kartkę w tym punkcie.
obraz_statku = zbuduj_statek(STATEK, KOLORY)
rysuj_tlo(ekran)
ekran.blit(obraz_statku, (392, 540))
Dwie rzeczy do zapamiętania. Po pierwsze, kartkę budujesz raz, przed pętlą, a w pętli tylko ją kładziesz. Nie ma powodu malować stu trzydziestu siedmiu pikseli od nowa sześćdziesiąt razy na sekundę, skoro statek się nie zmienia. Po drugie, punkt, który podajesz, to miejsce lewego górnego rogu kartki, nie jej środka. Kartka rozciąga się od tego punktu w prawo i w dół.
Linia jest w szufladzie MALUJ, po rysuj_tlo. W poprzedniej lekcji obiecałem, że zobaczysz, dlaczego tło ma iść pierwsze. Gdyby blit był przed nim, tło zamalowałoby statek w tej samej klatce, w której został położony, i okno byłoby puste. Sprawdziłem to i wyszło zero pikseli statku. Najpierw spód, potem to, co na spodzie.
Kartka ma szesnaście na szesnaście, a jej róg trafia w (392, 540). Policz, gdzie jest środek statku w poziomie i gdzie jego dolna krawędź w pionie.
W edytorze jest nagłówek funkcji i jej pierwsza linia. Dopisz resztę: wysokość z liczby wierszy, przezroczysta kartka, dwie pętle, jeden piksel na raz, oddana kartka. Wszystko, co do tego potrzebne, przeszło przez poprzednie kroki, więc nie ma tu nic nowego.
Statek będzie tam, gdzie w kroku 1, tyle że tym razem zbuduje go Twoja funkcja. Gdy werdykt jest zielony, kliknij przycisk ▶ Uruchom, zatrzymaj program przyciskiem STOP i kliknij przycisk Zwiń na pasku Kokpitu.
Ten sam statek co w kroku 1, w tym samym miejscu, przy (392, 540), ale tym razem złożyła go Twoja funkcja, piksel po pikselu. Każdy punkt w oknie ma swój znak w jednym z szesnastu wierszy. Zwiń Kokpit i kliknij Dalej.
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
CZARNY = (26, 28, 64)
STATEK = [
".......11.......",
".......44.......",
"......1441......",
"......1331......",
".....143341.....",
".....143341.....",
"....11433411....",
"...1143333411...",
"..114333333411..",
".11433355334411.",
"1143333553333411",
"1433333553333341",
"1133311551333311",
"..11.1677761.11.",
"......167761....",
".......1771.....",
]
KOLORY = {
"1": (18, 22, 48),
"3": (150, 170, 210),
"4": (225, 240, 255),
"5": (0, 200, 255),
"6": (255, 150, 40),
"7": (255, 230, 120),
}
def zbuduj_statek(ksztalt, kolory):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
for y in range(wysokosc):
for x in range(szerokosc):
znak = ksztalt[y][x]
if znak in kolory:
obraz.set_at((x, y), kolory[znak])
return obraz
def rysuj_tlo(ekran):
ekran.fill(CZARNY)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
obraz_statku = zbuduj_statek(STATEK, KOLORY)
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
rysuj_tlo(ekran)
ekran.blit(obraz_statku, (392, 540))
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
W edytorze masz cały blok danych: szesnaście linijek STATEK i słownik KOLORY. Pod nimi jest Twoja funkcja z poprzedniego kroku. To jest Twój statek, a te dwie rzeczy to jedyne, co o nim decyduje.
Możesz go zmienić. Przesuń znaki, zmień cyfry, dopisz do słownika nową cyfrę i daj jej własny kolor, wydłuż skrzydła albo zacznij od nowa. Możesz go też zostawić, jeśli Ci pasuje. Funkcja nie zna liczby szesnaście, tylko liczy rozmiar z danych, więc Twój statek może być szerszy, węższy albo wyższy niż nasz. Warunki są cztery: każda linijka tak samo długa, od czterech do trzydziestu dwóch znaków w linijce i tyle samo linijek, przynajmniej jedna cyfra, a każda użyta cyfra ma kolor w KOLORY.
To, co tu zostawisz, zostaje z Tobą. Od następnej lekcji w pliku jest Twój statek, nie nasz, i to on trafi do pliku, który pobierzesz na komputer. Kurs pamięta go w tej przeglądarce; jeśli otworzysz kurs gdzie indziej, na start dostaniesz z powrotem nasz i możesz go przerobić jeszcze raz.
W oknie będzie mały, bo tyle daje szesnaście pikseli w oknie osiemset na sześćset. W następnej lekcji urośnie trzykrotnie i dostanie swoje miejsce, więc jeśli chcesz coś w nim poprawić, zrób to teraz, póki widać każdy piksel: zwiń Kokpit przyciskiem Zwiń na jego pasku, popraw znaki i uruchom jeszcze raz. Kliknij przycisk ▶ Uruchom i zobacz go w oknie, a gdy wygląda tak, jak chcesz, kliknij przycisk Zwiń na pasku Kokpitu i przejdź dalej przyciskiem Dalej.
Z poprzedniej lekcji masz liczbę: sto trzydzieści siedem pomalowanych pikseli. W tej lekcji statek będzie trzy razy większy.
Zanim zobaczysz, jak to się robi, obstaw. Ile pomalowanych pikseli będzie miał po powiększeniu? Wpisz liczbę. Nikt jej nie ocenia; za chwilę ją zmierzymy i zestawimy z Twoim typem.
Funkcja dostaje trzeci parametr i kończy się inaczej:
def zbuduj_statek(ksztalt, kolory, skala):
return pygame.transform.scale(obraz,
(szerokosc * skala, wysokosc * skala))
pygame.transform.scale bierze gotową kartkę i oddaje nową, w podanym rozmiarze. Kartka szesnaście na szesnaście przy skali trzy staje się kartką czterdzieści osiem na czterdzieści osiem. Obok masz kod pomiaru, tylko do czytania. Buduje statek w skali trzy i liczy pomalowane piksele tak samo, jak w kroku 2 poprzedniej lekcji liczyło się znaki.
Tysiąc dwieście trzydzieści trzy, czyli sto trzydzieści siedem razy dziewięć. Nie razy trzy, bo statek rośnie trzy razy wszerz i trzy razy wzwyż, a z jednego kwadracika robi się blok trzy na trzy. scale nie maluje niczego nowego. Sprawdziłem: kolorów na kartce jest po powiększeniu dokładnie tyle samo co przed, bez żadnego rozmycia. Zwróć też uwagę, że rozmiar w nawiasie liczy się z szerokosc i wysokosc, nie z liczby szesnaście. Jeśli Twój statek ma inny rozmiar, urośnie tak samo poprawnie.
Duży statek nie może być tam, gdzie mały. Liczby (392, 540) były dobrane do kartki szesnaście na szesnaście. Z kartką czterdzieści osiem na czterdzieści osiem ten sam róg daje środek na czterysta szesnastym pikselu, nie na czterysetnym, a przy każdej zmianie rozmiaru trzeba by ten róg przeliczać od nowa. Pygame ma na to osobną rzecz.
prostokat = obraz.get_rect()
get_rect oddaje prostokąt: miejsce rzeczy, zapisane czterema liczbami. Dwie z nich, szerokość i wysokość, prostokąt bierze z obrazu. Dwie pozostałe, x i y, ma dostać od Ciebie. Prostokąt nie wie, gdzie był poprzedni obraz, i nie wie, gdzie jest środek okna. Wie tylko, jaki ma rozmiar.
Ktoś pobrał prostokąt i od razu, bez żadnej dalszej linii, położył nim statek na oknie. Gdzie się znalazł?
W lewym górnym rogu okna. Świeży prostokąt ma x zero i y zero, bo nikt mu jeszcze nie powiedział, gdzie ma być. Cała reszta tej lekcji to mówienie mu tego.
Prostokąt ma uchwyty. Każdy z nich to jedna krawędź albo jeden punkt: x to lewy brzeg, bottom to dolny, centerx to środek w poziomie. Chwytasz za jeden uchwyt, mówisz, gdzie ma się znaleźć, a prostokąt sam przelicza pozostałe trzy liczby. Nie liczysz rogu. Mówisz, gdzie ma być ten punkt, o który Ci chodzi.
O jakie punkty chodzi tutaj? Statek ma być na środku okna w poziomie i dwadzieścia pikseli nad dolną krawędzią. Dlatego chwytamy za centerx, a nie za x, i za bottom, a nie za y, bo to dół statku ma być w jednym miejscu, niezależnie od tego, jak wysoki jest statek. Szerokość i wysokość okna znasz ze stałych na górze pliku od pierwszej lekcji.
Wypełnij dwie luki w ciele nowy_statek.
Na rysunku obok ktoś ustawił statek tak, że widać tylko jego górną połowę, przy samej dolnej krawędzi okna. Dolna połowa jest pod oknem. Ta osoba chwyciła prostokąt za inny uchwyt niż Ty w poprzednim kroku, a liczbę podała tę samą.
Które linie dają dokładnie ten obraz? Zaznacz wszystkie, które pasują; może być więcej niż jedna.
Ta sama liczba, trzy uchwyty, trzy różne obrazy: bottom zostawia statek w całości, centery, a tak samo druga liczba w center, chowa dolną połowę pod okno, a y zostawia sam skrawek. Uchwyt mówi, która krawędź ląduje na liczbie; liczba sama z siebie nie mówi nic.
W edytorze są dwie rzeczy. Pierwsza to Twoja funkcja zbuduj_statek z luką na końcu, bo w return brakuje skali, a trzeci parametr już czeka w nagłówku. Druga to pusty nagłówek def nowy_statek(obraz):. Napisz jej ciało: pobierz prostokąt, chwyć go za dwa uchwyty i oddaj kartę statku.
Karta statku to słownik, taki sam jak w Podstawach, ale po raz pierwszy opisuje rzecz, która żyje w grze. Ma trzy wpisy: "obraz", czyli jak statek wygląda, "prostokat", czyli gdzie jest, i "predkosc" równą sześć, czyli jak szybko pójdzie, gdy ruszy. Na razie się nie rusza. Tę liczbę wykorzystasz za dwie lekcje.
W main zmieniają się trzy linie i te masz gotowe:
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
statek = nowy_statek(obraz_statku)
ekran.blit(statek["obraz"], statek["prostokat"])
blit przyjmuje prostokąt zamiast pary liczb i sam bierze z niego róg. Liczby (392, 540) znikają z pliku na dobre.
Statek będzie trzy razy większy i znajdzie się na środku dołu okna, dwadzieścia pikseli nad krawędzią. Gdy werdykt jest zielony, kliknij przycisk ▶ Uruchom, a potem kliknij przycisk Zwiń na pasku Kokpitu.
Statek trzy razy większy, na środku dołu okna, dwadzieścia pikseli nad krawędzią, a w pliku nie ma już (392, 540): jego miejsce bierze się z prostokąta w karcie. Prędkość sześć czeka w tej samej karcie i na razie nic nie robi, użyjesz jej za dwie lekcje. Zwiń Kokpit i kliknij Dalej.
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
CZARNY = (26, 28, 64)
STATEK = [
".......11.......",
".......44.......",
"......1441......",
"......1331......",
".....143341.....",
".....143341.....",
"....11433411....",
"...1143333411...",
"..114333333411..",
".11433355334411.",
"1143333553333411",
"1433333553333341",
"1133311551333311",
"..11.1677761.11.",
"......167761....",
".......1771.....",
]
KOLORY = {
"1": (18, 22, 48),
"3": (150, 170, 210),
"4": (225, 240, 255),
"5": (0, 200, 255),
"6": (255, 150, 40),
"7": (255, 230, 120),
}
def zbuduj_statek(ksztalt, kolory, skala):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
for y in range(wysokosc):
for x in range(szerokosc):
znak = ksztalt[y][x]
if znak in kolory:
obraz.set_at((x, y), kolory[znak])
return pygame.transform.scale(obraz,
(szerokosc * skala, wysokosc * skala))
def nowy_statek(obraz):
prostokat = obraz.get_rect()
prostokat.centerx = SZEROKOSC // 2
prostokat.bottom = WYSOKOSC - 20
return {"obraz": obraz, "prostokat": prostokat, "predkosc": 6}
def rysuj_tlo(ekran):
ekran.fill(CZARNY)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
statek = nowy_statek(obraz_statku)
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
rysuj_tlo(ekran)
ekran.blit(statek["obraz"], statek["prostokat"])
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Kartka urosła trzy razy. W poprzedniej lekcji jej rogi, tam, gdzie w siatce były kropki, były przezroczyste i prześwitywał przez nie granat.
Odpowiedz z głowy, bez uruchamiania i bez zaglądania do kodu. Co jest teraz w rogach dużej kartki?
Nadal są przezroczyste. scale powiększa to, co jest na kartce, a przezroczystość jest na niej tak samo jak kolor. Sprawdziłem na dużej kartce: rogi przepuszczają wszystko. W następnej lekcji pod statkiem staną gwiazdy i wtedy przezroczyste rogi przestaną być teorią.
Ta lekcja zaczyna się od pisania, nie od okna, bo do pierwszej funkcji nie potrzebujesz nic nowego. Niebo to dziewięćdziesiąt gwiazd: jedna gwiazda to krotka trzech liczb, całe niebo to lista takich krotek, a losowanie robi random, ten sam co w Podstawach.
Napisz losuj_gwiazdy(ile). Ma oddać listę ile gwiazd: x od zera do SZEROKOSC, y od zera do WYSOKOSC, a jasność wybraną z czterech gotowych liczb, czyli 70, 110, 160 albo 210. Gwiazdy są losowe, więc nie ma jednej dobrej odpowiedzi; sprawdzimy własności listy, nie jej zawartość.
Dwie rzeczy w tej funkcji są wyborem, nie przypadkiem. Jasność ma cztery wartości, a nie dowolną liczbę od zera do dwustu pięćdziesięciu pięciu, bo prawdziwe niebo ma kilka rodzajów gwiazd, a nie szum. Liczba dziewięćdziesiąt nie stoi w środku funkcji, tylko wchodzi przez nawias jako ile, bo niebo dostanie kiedyś inną liczbę i wtedy nie będziesz szukać jej w kodzie. Funkcja jest gotowa. Gdzie stanie jej wywołanie, zdecyduje o wszystkim, ale to za dwa kroki.
W Kokpicie leży plik po tej lekcji. Zanim klikniesz, wiedz, na co patrzeć. Gwiazdy nie będą się ruszać, statek też nie. Przyjrzyj się temu niebu przez chwilę i zapamiętaj je: kilka jaśniejszych gwiazd, dużo słabych, wszystkie w miejscu. Za dwa kroki zobaczysz drugie niebo, które nie zostaje w miejscu. Kliknij przycisk ▶ Uruchom, popatrz na niebo, zatrzymaj program przyciskiem STOP i kliknij przycisk Zwiń na pasku Kokpitu. Tekst wróci i przejdziesz do następnego kroku przyciskiem Dalej.
rysuj_tlo dostaje drugie ucho i pętlę:
def rysuj_tlo(ekran, gwiazdy):
for x, y, jasnosc in gwiazdy:
Drugie ucho to ten sam rytuał, co w lekcji o pierwszym pokoju. Funkcja nie widzi listy z main, więc lista wchodzi przez nawias. Pętla rozpakowuje każdą krotkę na trzy nazwy naraz, dokładnie tak, jak w Podstawach rozpakowywało się pary.
Do rysowania gwiazdy potrzebna jest nowa przegródka pudełka pygame, o nazwie draw. Leżą w niej gotowe kształty, a każdy dostaje to samo: na czym rysować, jakim kolorem, gdzie i jak duży. Gwiazda to kółko o promieniu jeden. Sprawdziłem, ile to jest. Promień jeden zapala blok dwa na dwa piksele i to wystarcza, żeby oko ją zauważyło. Kolor to trzy liczby, jak zawsze, ale nie trzy równe, tylko (jasnosc, jasnosc, jasnosc + 20). Odrobina niebieskiego więcej sprawia, że gwiazdy są lekko chłodne, a nie po prostu białe.
Wypełnij dwie luki w linii rysowania.
W Kokpicie leży kopia pliku z tej lekcji, w której ktoś przeniósł jedną linię w inne miejsce. Zobaczysz niebo, które miga. Zmierzyłem to: między dwiema kolejnymi klatkami ani jedna gwiazda nie zostaje na swoim miejscu. Kliknij przycisk ▶ Uruchom, popatrz chwilę na to niebo, zatrzymaj program przyciskiem STOP i kliknij przycisk Zwiń na pasku Kokpitu.
Ile razy na sekundę losują się w tej wersji gwiazdy?
Sześćdziesiąt, bo tyle obiegów na sekundę robi metronom, a wywołanie losuj_gwiazdy stoi w tej kopii w pętli, tuż przed rysowaniem tła. Każdy obieg dostaje nowe niebo. Z tego objawu wynika zasada, która obowiązuje w całym kursie: stan świata ustalasz raz, przed pętlą, a pętla go tylko rysuje. Gwiazdy są pierwszym takim stanem, o którym mówię wprost, ale nie pierwszym, jaki masz w pliku. Statek też powstaje przed pętlą: w poprzedniej lekcji nowy_statek trafił nad while, zanim było wiadomo, że to zasada.
Masz main z poprzedniej lekcji rozbitą na klocki i dwie nowe linie tej lekcji do wstawienia. Ułóż całość tak, żeby niebo zostało w miejscu. Linie przed pętlą mogą być w dowolnej kolejności między sobą; liczy się to, co jest przed while, a co w środku, i porządek w szufladzie MALUJ.
Niebo zostanie w miejscu. Przy okazji jedno zdanie o tym, czego nie widać. W rysuj_tlo malowanie tła jest przed pętlą gwiazd. Gdyby było po niej, zamalowałoby wszystkie dziewięćdziesiąt i niebo byłoby puste. To ten sam porządek co ze statkiem: najpierw spód. Gdy werdykt jest zielony, kliknij przycisk ▶ Uruchom, a potem kliknij przycisk Zwiń na pasku Kokpitu.
Dziewięćdziesiąt gwiazd, każda na swoim miejscu, klatka po klatce te same, bo wylosowały się raz, przed pętlą, a w pętli tylko się rysują. Tło pod nimi, gwiazdy na wierzchu: ten sam porządek co ze statkiem. Zwiń Kokpit i kliknij Dalej.
import random
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
CZARNY = (26, 28, 64)
STATEK = [
".......11.......",
".......44.......",
"......1441......",
"......1331......",
".....143341.....",
".....143341.....",
"....11433411....",
"...1143333411...",
"..114333333411..",
".11433355334411.",
"1143333553333411",
"1433333553333341",
"1133311551333311",
"..11.1677761.11.",
"......167761....",
".......1771.....",
]
KOLORY = {
"1": (18, 22, 48),
"3": (150, 170, 210),
"4": (225, 240, 255),
"5": (0, 200, 255),
"6": (255, 150, 40),
"7": (255, 230, 120),
}
def zbuduj_statek(ksztalt, kolory, skala):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
for y in range(wysokosc):
for x in range(szerokosc):
znak = ksztalt[y][x]
if znak in kolory:
obraz.set_at((x, y), kolory[znak])
return pygame.transform.scale(obraz,
(szerokosc * skala, wysokosc * skala))
def losuj_gwiazdy(ile):
gwiazdy = []
for numer in range(ile):
x = random.randint(0, SZEROKOSC)
y = random.randint(0, WYSOKOSC)
jasnosc = random.choice([70, 110, 160, 210])
gwiazdy.append((x, y, jasnosc))
return gwiazdy
def nowy_statek(obraz):
prostokat = obraz.get_rect()
prostokat.centerx = SZEROKOSC // 2
prostokat.bottom = WYSOKOSC - 20
return {"obraz": obraz, "prostokat": prostokat, "predkosc": 6}
def rysuj_tlo(ekran, gwiazdy):
ekran.fill(CZARNY)
for x, y, jasnosc in gwiazdy:
pygame.draw.circle(ekran, (jasnosc, jasnosc, jasnosc + 20), (x, y), 1)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
gwiazdy = losuj_gwiazdy(90)
statek = nowy_statek(obraz_statku)
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
rysuj_tlo(ekran, gwiazdy)
ekran.blit(statek["obraz"], statek["prostokat"])
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Statek jest na środku dołu okna, nad nim gwiazdy. W następnej lekcji ruszy: sześć pikseli na klatkę, sześćdziesiąt klatek na sekundę.
Od miejsca, w którym jest, do zniknięcia za prawą krawędzią okna: ile to sekund? Wpisz swój typ. Nie licz, obstaw z wyczucia. Rozliczymy go w następnej lekcji, gdy statek naprawdę poleci.
Siedem lekcji temu ten plik miał dwadzieścia trzy puste nazwy. Dziś w oknie jest Twój statek na niebie z gwiazd, a przez cały ten czas ani razu nie trzeba było składać pętli gry od zera. Zawsze była w pliku, a każda lekcja dopisywała do niej po jednej linii. Ten egzamin jest po to, żeby sprawdzić, ile z niej masz w głowie, a nie na ekranie obok.
Jedna zasada na trzy kroki: nie zaglądasz do poprzednich lekcji. Nie dlatego, że to zabronione. Dlatego, że z otwartą lekcją obok odpowiedź napisze się sama, a Ty nie dowiesz się o sobie niczego. Zamknij je, zostaw ten jeden krok i zobacz, co zostało.
Poniżej leżą wszystkie linie pętli gry, tak jak wygląda w Twoim pliku po lekcji o gwiazdach, ale pomieszane i bez wcięć. Ułóż je w kolejności, w jakiej wykonuje je jeden obieg, i wsuń każdą tak głęboko, jak stoi w pliku. Trzy szuflady i ich porządek masz z lekcji o metronomie; tu nikt Ci ich nie przypomni i o to chodzi.
nowy_statek od zeraW lekcji o powiększaniu ta funkcja powstawała z podpowiedzią obok. Nagłówek był, uchwyty były nazwane, brakowało dwóch luk. Teraz jest tylko nagłówek i pusta linia pod nim.
Napisz nowy_statek tak, jak wygląda w Twoim pliku: weź prostokąt z obrazu, umieść go tam, gdzie jest statek, czyli na środku okna w poziomie i dwadzieścia pikseli nad dolną krawędzią, i oddaj kartę statku z obrazem, prostokątem i prędkością sześć. Za które uchwyty chwycić i skąd wziąć liczby, masz w głowie od dwóch lekcji; tu tylko to sprawdzasz.
Okna w tym kroku nie ma i nie potrzebujesz go. Werdykt pod edytorem czyta oddaną kartę: gdzie jest prostokąt, jaką ma prędkość i czy w słowniku jest wszystko, co powinno.
Ostatni krok nie ma nic do uruchomienia i to jest jego zaleta. Komputer przeszedłby tę pętlę w ułamku sekundy i pokazał wynik, a nie byłoby wiadomo, czy rozumiesz, czy tylko patrzysz. Tym razem pętlę wykonujesz Ty, przy stole, tak jak w Podstawach śledziło się kod palcem.
Masz małą siatkę, dwa wiersze po cztery znaki, i słownik z jednym kolorem. Twoja funkcja z lekcji o statku idzie po niej tak, jak zawsze: wiersz po wierszu, a w wierszu znak po znaku, i maluje piksel, gdy znak ma kolor. Prześledź trzy pierwsze malowania i zapisz, w którym miejscu każde z nich położyło piksel, w kolejności, w jakiej się wykonały.
Zanim wpiszesz, przypomnij sobie z lekcji o statku, na czym polegała pomyłka, która położyła go na boku. Siatka zadaje jedno pytanie, kartka drugie, i każde ma własną kolejność. Tu sprawdzasz, czy tamten dzień został.
Masz w głowie cały szkielet tej gry: pętlę, która słucha, liczy i maluje, statek z kartą i sposób, w jaki z cyfr robi się obraz. Wszystko, co dojdzie do pliku od następnej lekcji, stanie na tym szkielecie. W następnej lekcji statek ruszy.
Trzy szuflady z lekcji o metronomie: SŁUCHAJ jest pełna od pięciu lekcji, MALUJ od czterech, a LICZ jest pusta od dnia, w którym ją narysowaliśmy. Dziś wchodzi do niej pierwsza linia. Jedna. I statek zaczyna słuchać rąk.
steruj_statkiem(statek, pygame.key.get_pressed())
pygame.key.get_pressed to pytanie do klawiatury. Nie ma nic wspólnego ze skrzynką z karteczkami, którą otwierasz w szufladzie SŁUCHAJ; to osobne pytanie i zadaje się je wprost, w każdym obiegu. Odpowiedź to lista, w której pod nazwą każdego klawisza jest True albo False. Ta lista wchodzi do funkcji sterowania jako klawisze.
Trzymasz strzałkę w prawo przez dziesięć klatek. W ilu z tych dziesięciu klatek pod klawiszem w prawo jest True?
We wszystkich dziesięciu. Skrzynka mówi, co się stało: jedna karteczka na jedno wciśnięcie, a potem cisza, choćbyś trzymał klawisz minutę. get_pressed mówi, jak jest: dopóki trzymasz, odpowiedź brzmi „tak". Ruch statku to drugi rodzaj, bo ma trwać tak długo, jak trzymasz strzałkę. Odpowiedź z get_pressed jest zdjęciem klawiatury z tej jednej chwili, dlatego robi się je co klatkę, w pętli. Sprawdziłem, co się dzieje, gdy zrobić je raz, przed pętlą: statek nigdy nie rusza, bo zdjęcie zostało zrobione, gdy nic nie było wciśnięte.
ifFunkcja sterowania wyciąga z karty statku prostokąt i dla każdej strzałki robi to samo. Pyta, czy klawisz jest wciśnięty, i jeśli tak, przesuwa uchwyt prostokąta o prędkość z karty. Pierwszy kierunek masz gotowy:
def steruj_statkiem(statek, klawisze):
prostokat = statek["prostokat"]
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
Nazwy klawiszy są gotowe, leżą w pudełku pygame i są po angielsku, tak samo jak QUIT: K_LEFT, K_RIGHT, K_UP, K_DOWN. prostokat.x to uchwyt z poprzedniej lekcji, lewy brzeg, a szóstka z karty w końcu do czegoś służy. Jeszcze przypomnienie o osiach, bo za chwilę się przyda. y rośnie w dół, tak jak numery wierszy w siatce statku.
Dopisz drugi kierunek, w prawo. Dwie luki.
Dwie wersje tej samej funkcji. Lewa to nasza, z czterema osobnymi if. Prawa jest cudza i wygląda porządniej: jeden if i trzy elif, jak drabinka z Podstaw.
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
if klawisze[pygame.K_RIGHT]:
prostokat.x += statek["predkosc"]
if klawisze[pygame.K_UP]:
prostokat.y -= statek["predkosc"]
if klawisze[pygame.K_DOWN]:
prostokat.y += statek["predkosc"]
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
elif klawisze[pygame.K_RIGHT]:
prostokat.x += statek["predkosc"]
elif klawisze[pygame.K_UP]:
prostokat.y -= statek["predkosc"]
elif klawisze[pygame.K_DOWN]:
prostokat.y += statek["predkosc"]
Pojedyncze strzałki działają w obu wersjach tak samo. Różnica wychodzi, gdy trzymasz dwie naraz. W prawej wersji trzymasz w lewo i w górę jednocześnie. Gdzie poleci statek?
Tylko w lewo. elif dostaje głos wyłącznie wtedy, gdy poprzedni warunek był fałszywy. Lewo było prawdziwe, więc reszta drabinki nie jest nawet sprawdzana i góra nie ma szans. Dlatego w sterowaniu są cztery niezależne pytania, a nie jedno z odgałęzieniami: dwa klawisze mogą być wciśnięte w tej samej chwili i oba mają się liczyć. Tak powstaje skos. To nie jest reguła do zapamiętania, tylko powód: drabinka wybiera jedną odpowiedź, a Ty chcesz dwóch.
W edytorze jest nagłówek i pierwsza linia, ta z prostokątem. Dopisz resztę: cztery strzałki, cztery if, cztery przesunięcia o prędkość z karty. Werdykt sprawdza funkcję na sztucznych klawiszach, także na dwóch naraz.
Zanim klikniesz, zapamiętaj kolejność: najpierw klik w okno gry, dopiero potem strzałki. Dopóki nie klikniesz w okno gry, klawiatura idzie do strony, nie do gry, i statek się nie rusza, choć Twój kod jest dobry. Po kliknięciu strzałki działają. Gdy werdykt jest zielony, kliknij przycisk ▶ Uruchom, kliknij w okno gry, polataj strzałkami, a potem kliknij przycisk Zwiń na pasku Kokpitu.
Statek jedzie tam, gdzie każą strzałki, i po raz pierwszy w tym kursie coś w oknie reaguje na Ciebie. Prędkość sześć z karty, obiecana dwie lekcje temu, właśnie zaczęła pracować; dwie strzałki naraz dają skos. Zwiń Kokpit i kliknij Dalej.
import random
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
CZARNY = (26, 28, 64)
STATEK = [
".......11.......",
".......44.......",
"......1441......",
"......1331......",
".....143341.....",
".....143341.....",
"....11433411....",
"...1143333411...",
"..114333333411..",
".11433355334411.",
"1143333553333411",
"1433333553333341",
"1133311551333311",
"..11.1677761.11.",
"......167761....",
".......1771.....",
]
KOLORY = {
"1": (18, 22, 48),
"3": (150, 170, 210),
"4": (225, 240, 255),
"5": (0, 200, 255),
"6": (255, 150, 40),
"7": (255, 230, 120),
}
def zbuduj_statek(ksztalt, kolory, skala):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
for y in range(wysokosc):
for x in range(szerokosc):
znak = ksztalt[y][x]
if znak in kolory:
obraz.set_at((x, y), kolory[znak])
return pygame.transform.scale(obraz,
(szerokosc * skala, wysokosc * skala))
def losuj_gwiazdy(ile):
gwiazdy = []
for numer in range(ile):
x = random.randint(0, SZEROKOSC)
y = random.randint(0, WYSOKOSC)
jasnosc = random.choice([70, 110, 160, 210])
gwiazdy.append((x, y, jasnosc))
return gwiazdy
def nowy_statek(obraz):
prostokat = obraz.get_rect()
prostokat.centerx = SZEROKOSC // 2
prostokat.bottom = WYSOKOSC - 20
return {"obraz": obraz, "prostokat": prostokat, "predkosc": 6}
def steruj_statkiem(statek, klawisze):
prostokat = statek["prostokat"]
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
if klawisze[pygame.K_RIGHT]:
prostokat.x += statek["predkosc"]
if klawisze[pygame.K_UP]:
prostokat.y -= statek["predkosc"]
if klawisze[pygame.K_DOWN]:
prostokat.y += statek["predkosc"]
def rysuj_tlo(ekran, gwiazdy):
ekran.fill(CZARNY)
for x, y, jasnosc in gwiazdy:
pygame.draw.circle(ekran, (jasnosc, jasnosc, jasnosc + 20), (x, y), 1)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
gwiazdy = losuj_gwiazdy(90)
statek = nowy_statek(obraz_statku)
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
steruj_statkiem(statek, pygame.key.get_pressed())
rysuj_tlo(ekran, gwiazdy)
ekran.blit(statek["obraz"], statek["prostokat"])
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Została jedna linia: wywołanie. Masz pętlę main rozbitą na klocki, z trzema szufladami zaznaczonymi, i klocek steruj_statkiem(statek, pygame.key.get_pressed()) do wstawienia. Szuflada LICZ była pusta przez pięć lekcji, bo czekała na pierwszą rzecz, która się rusza. To jest ona. Wstaw klocek na właściwe miejsce i na właściwą głębokość.
Sterowanie ma się dziać w każdym obiegu, nie tylko wtedy, gdy do skrzynki wpadnie karteczka, i ma stać przed malowaniem, żeby okno pokazało statek już po przesunięciu. Uczciwie: gdyby dziś stało po rysuj_tlo, oko nie zobaczyłoby różnicy, bo to jedna klatka opóźnienia, sześć pikseli. Porządek trzymamy dla następnych rzeczy, które trafią do tej szuflady, a będzie ich sporo.
W Kokpicie leży plik po tej lekcji. Zanim klikniesz, zapamiętaj kolejność: najpierw klik w okno gry, bo bez tego klawiatura nie trafia do gry, dopiero potem strzałki. Cztery kierunki, skos też. Statek płynie, nie skacze, bo działa metronom z lekcji trzeciej, sześćdziesiąt małych kroków na sekundę. Kliknij przycisk ▶ Uruchom, kliknij w okno gry, polataj chwilę, a gdy skończysz, zatrzymaj program przyciskiem STOP i kliknij przycisk Zwiń na pasku Kokpitu. Tekst wróci i przejdziesz do następnego kroku przyciskiem Dalej.
W poprzedniej lekcji był zakład: ile sekund zajmie statkowi dolot do prawej krawędzi. Obok masz swój typ i kod pomiaru, tylko do czytania. Przesuwa x o sześć, aż wyjdzie poza okno, i liczy klatki.
Siedemdziesiąt jeden klatek, czyli około jednej i dwóch dziesiątych sekundy. Rachunek jest z małych liczb: lewy brzeg statku stoi na trzysta siedemdziesiątym szóstym pikselu, okno kończy się na osiemsetnym, więc do przejścia są czterysta dwadzieścia cztery piksele. Po sześć na klatkę to siedemdziesiąt jeden klatek, a przy sześćdziesięciu klatkach na sekundę to nieco ponad sekunda. Całe okno, od brzegu do brzegu, statek przelatuje w nieco ponad dwie sekundy. To jest zapłata za pytanie z lekcji o metronomie: sześć pikseli na klatkę i widać ruch, nie skoki.
Ostatni krok to gra, bez kodu. Zanim klikniesz, zapamiętaj kolejność: klik w okno gry, potem przytrzymana strzałka w dół, a Ty patrzysz, co robi statek przy dolnej krawędzi. Gdy zobaczysz, puść strzałkę, wciśnij strzałkę w górę i sprawdź to samo z każdą krawędzią. Kliknij przycisk ▶ Uruchom, kliknij w okno gry i przytrzymaj strzałkę w dół, a gdy sprawdzisz wszystkie krawędzie, zatrzymaj program przyciskiem STOP i kliknij przycisk Zwiń na pasku Kokpitu.
Statek zniknął pod krawędzią po dwunastu klatkach, czyli w jedną piątą sekundy, bo był tylko dwadzieścia pikseli nad dołem. Nic go nie zatrzymało, program nie zgłosił błędu, gra szła dalej z pustym oknem, a po strzałce w górę statek wrócił. To jest celowe. Nic w Twoim pliku nie wie, gdzie kończy się okno, więc statek leci tam, gdzie każą mu strzałki. W następnej lekcji dopiszesz jedną linię, po której zatrzyma się na krawędzi. Tekst wrócił, więc przejdź do następnej lekcji przyciskiem Dalej.
Na końcu poprzedniej lekcji statek zniknął pod dolną krawędzią okna i nic się nie stało. Program działał dalej, okno zostało puste, a Python nie zgłosił żadnego błędu. Z jego punktu widzenia wszystko było w porządku: statek ma swoje x i y, co klatkę dostaje nowe, a to, że okno kończy się na osiemsetnym pikselu, nie jest zapisane nigdzie w Twoim pliku.
Zanim cokolwiek naprawisz, policz, ile tego statku ucieka. Przytrzymujesz strzałkę w prawo tak długo, aż lewy brzeg statku dojedzie do x = 790. Statek ma 48 pikseli szerokości, a okno 800. Gdzie wtedy jest prawy brzeg statku i ile jego pikseli jest już poza oknem? Kliknij Uruchom pomiar i porównaj wynik z tym, co wyszło Ci w głowie.
Prawy brzeg to lewy brzeg plus szerokość statku, a to, co wystaje, to prawy brzeg minus szerokość okna. Jedno dodawanie i jedno odejmowanie: tyle matematyki wystarczy, żeby wiedzieć, o ile statek trzeba cofnąć. Za chwilę zobaczysz, że ktoś zrobił dokładnie to.
Ktoś, kto buduje tę samą grę, rozwiązał to po swojemu. Pod czterema warunkami sterowania dopisał cztery kolejne, po jednym na każdą krawędź. Jeśli lewy brzeg statku jest mniejszy od zera, ustaw go na zero. Jeśli prawy brzeg wyszedł poza szerokość okna, ustaw go na szerokość okna. To samo dla górnej i dolnej krawędzi. Każdy warunek to dwie linie, więc razem jest ich osiem.
Przeczytaj ten kod uważnie i zdecyduj, co jest z nim nie tak. Nie oceniaj go po wyglądzie. Prześledź, co robi ze statkiem przy każdej z czterech krawędzi, i dopiero wtedy wybierz odpowiedź.
Program nie musi być krótki, żeby był poprawny. Te osiem linii pilnuje wszystkich czterech krawędzi i statek zostaje w oknie. Jedyne, co można im zarzucić, to długość: cztery razy ta sama myśl. Następne dwa kroki są o tym, jak tę samą myśl zapisać raz.
Zanim cokolwiek skrócisz, napisz to samo własnoręcznie dla jednej osi, żeby wiedzieć, co dokładnie skracasz. Napisz funkcję wepchnij_x(x, szerokosc_statku, szerokosc_okna). Dostaje położenie lewego brzegu statku i dwie szerokości, a oddaje takie x, przy którym cały statek mieści się w oknie. Jeśli statek wystaje z lewej, ma wrócić na lewą krawędź. Jeśli wystaje z prawej, ma wrócić tak, żeby jego prawy brzeg dotykał prawej krawędzi. Jeśli nie wystaje wcale, x ma zostać bez zmian.
Wystarczą dwa if-y. Nie ma tu żadnej sztuczki, tylko liczby z pierwszego kroku: gdzie jest prawy brzeg i o ile za daleko. Sprawdzenie uruchomi Twoją funkcję na trzech przypadkach: za mało, za dużo i w sam raz.
Policz linie w swojej funkcji. Dwa warunki, w każdym jedno przypisanie: cztery linie na jedną oś. Ta sama funkcja dla y to kolejne cztery, razem osiem. Dokładnie tyle, ile miał kod z poprzedniego kroku. Nie skróciło się nic, bo arytmetyka jest ta sama. Skrót czeka gdzie indziej.
Pygame umie to zrobić za Ciebie, ale trzeba mu opisać, w czym statek ma się zmieścić. Do tego służy pygame.Rect, który znasz jako ramkę obrazka. Ramkę można też zbudować z czterech liczb: pygame.Rect(x, y, szerokosc, wysokosc). Dwie pierwsze mówią, gdzie ramka się zaczyna, dwie kolejne, jaka jest duża. Taka ramka nie musi mieć w środku żadnego obrazka. To po prostu opis prostokątnego kawałka ekranu: może to być całe okno, jego połowa albo dowolny inny obszar.
Druga nowość to clamp_ip. Wywołujesz je na ramce statku i podajesz mu drugą ramkę. Jeśli ramka statku wystaje poza tę drugą, clamp_ip przesuwa ją do środka o tyle, ile trzeba. Jeśli się mieści, nie robi nic. Rozmiaru ramki nie zmienia nigdy: statek po wepchnięciu jest dokładnie tak samo szeroki i wysoki jak przed. Końcówka _ip to skrót od angielskiego „in place", czyli „w miejscu": ramka statku zmienia się sama, niczego nie trzeba przypisywać.
Uzupełnij dwie luki tak, żeby drugą ramką było całe okno gry. Program wypisze prawy brzeg statku po wepchnięciu. Z pierwszego kroku wiesz już, jaka liczba oznacza, że statek dotyka krawędzi i nic z niego nie wystaje.
Ta jedna linia robi to samo, co osiem linii z drugiego kroku i obie Twoje funkcje z trzeciego. Cztery krawędzie, dwie osie, jedno polecenie. Różnica nie jest w wyniku, bo statek ląduje w tym samym miejscu. Różnica jest w tym, ile trzeba przeczytać, żeby zrozumieć, co program robi.
Tak wygląda Twoja funkcja sterowania w tej chwili:
def steruj_statkiem(statek, klawisze):
prostokat = statek["prostokat"]
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
if klawisze[pygame.K_RIGHT]:
prostokat.x += statek["predkosc"]
if klawisze[pygame.K_UP]:
prostokat.y -= statek["predkosc"]
if klawisze[pygame.K_DOWN]:
prostokat.y += statek["predkosc"]
Dopisz na jej końcu jedną linię, na tym samym wcięciu co cztery warunki:
prostokat.clamp_ip(pygame.Rect(0, 0, SZEROKOSC, WYSOKOSC))
SZEROKOSC i WYSOKOSC masz w pliku od pierwszej lekcji, więc ramka okna bierze się z tych samych liczb, z których powstało samo okno.
Miejsce tej linii ma znaczenie i to jest jedyna rzecz, o której trzeba tu pamiętać. Musi być po wszystkich czterech warunkach. Każdy warunek przesuwa statek, a clamp_ip poprawia jego położenie. Jeśli poprawka znajdzie się wyżej, najpierw wepchniesz statek do okna, a zaraz potem któryś warunek znowu wypchnie go poza krawędź i na ekranie nie zmieni się nic. Poprawka musi być ostatnim słowem w tej funkcji.
Zanim klikniesz, zapamiętaj plan: klikasz w okno gry, przytrzymujesz strzałkę w prawo i patrzysz, co robi statek przy prawej krawędzi. Kliknij ▶ Uruchom, kliknij w okno gry i przytrzymaj strzałkę w prawo.
Statek dojechał do prawej krawędzi i został na niej cały widoczny, choć strzałka jest wciąż wciśnięta. Co klatkę ruch wypycha go o 6 pikseli poza okno, a clamp_ip w tej samej klatce cofa go z powrotem, więc na ekranie widzisz go w jednym miejscu. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie już przycisk Dalej.
import random
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
CZARNY = (26, 28, 64)
STATEK = [
".......11.......",
".......44.......",
"......1441......",
"......1331......",
".....143341.....",
".....143341.....",
"....11433411....",
"...1143333411...",
"..114333333411..",
".11433355334411.",
"1143333553333411",
"1433333553333341",
"1133311551333311",
"..11.1677761.11.",
"......167761....",
".......1771.....",
]
KOLORY = {
"1": (18, 22, 48),
"3": (150, 170, 210),
"4": (225, 240, 255),
"5": (0, 200, 255),
"6": (255, 150, 40),
"7": (255, 230, 120),
}
def zbuduj_statek(ksztalt, kolory, skala):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
for y in range(wysokosc):
for x in range(szerokosc):
znak = ksztalt[y][x]
if znak in kolory:
obraz.set_at((x, y), kolory[znak])
return pygame.transform.scale(obraz,
(szerokosc * skala, wysokosc * skala))
def losuj_gwiazdy(ile):
gwiazdy = []
for numer in range(ile):
x = random.randint(0, SZEROKOSC)
y = random.randint(0, WYSOKOSC)
jasnosc = random.choice([70, 110, 160, 210])
gwiazdy.append((x, y, jasnosc))
return gwiazdy
def nowy_statek(obraz):
prostokat = obraz.get_rect()
prostokat.centerx = SZEROKOSC // 2
prostokat.bottom = WYSOKOSC - 20
return {"obraz": obraz, "prostokat": prostokat, "predkosc": 6}
def steruj_statkiem(statek, klawisze):
prostokat = statek["prostokat"]
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
if klawisze[pygame.K_RIGHT]:
prostokat.x += statek["predkosc"]
if klawisze[pygame.K_UP]:
prostokat.y -= statek["predkosc"]
if klawisze[pygame.K_DOWN]:
prostokat.y += statek["predkosc"]
prostokat.clamp_ip(pygame.Rect(0, 0, SZEROKOSC, WYSOKOSC))
def rysuj_tlo(ekran, gwiazdy):
ekran.fill(CZARNY)
for x, y, jasnosc in gwiazdy:
pygame.draw.circle(ekran, (jasnosc, jasnosc, jasnosc + 20), (x, y), 1)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
gwiazdy = losuj_gwiazdy(90)
statek = nowy_statek(obraz_statku)
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
steruj_statkiem(statek, pygame.key.get_pressed())
rysuj_tlo(ekran, gwiazdy)
ekran.blit(statek["obraz"], statek["prostokat"])
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Liczby z poprzedniego kroku mówią, że statek dotyka krawędzi. Tym razem to zobaczysz. Ten krok nie wypisze liczby, tylko narysuje obrazek: małą planszę z kwadratem, który udaje statek. Ta plansza nie jest oknem gry, jest od niego dużo mniejsza, ale ramka, którą podasz do clamp_ip, działa na niej tak samo.
Uzupełnij jedną lukę tak, żeby kwadrat został wepchnięty do planszy, i uruchom sprawdzenie. Na obrazku szukaj kwadratu, który dotyka prawej krawędzi planszy i jest widoczny w całości. Jeśli krawędź ucięła jakiś jego kawałek, luka jest wypełniona źle. Gdy zobaczysz cały kwadrat przy krawędzi, ten krok jest zaliczony i możesz przejść do ostatniego.
Przed wepchnięciem kwadrat wystawał poza planszę o kawałek. Po wepchnięciu jest o dokładnie ten kawałek bliżej i ani o piksel dalej. To jest cała robota clamp_ip: nie odsuwa na zapas, nie zostawia marginesu, przesuwa dokładnie o tyle, ile wystaje.
Twoja gra ma od tej lekcji granice ze wszystkich czterech stron, choć w pliku przybyła jedna linia. Sprawdź wszystkie naraz. Plan: klikasz w okno gry, przytrzymujesz dwie strzałki jednocześnie, na przykład w prawo i w dół, i jedziesz do rogu. Statek ma dojechać do rogu i zostać w nim cały widoczny, dotykając krawędzi z obu stron. Kliknij ▶ Uruchom, kliknij w okno gry i przytrzymaj dwie strzałki naraz.
Statek zostanie w rogu, cały widoczny, i nie zniknie tak jak na końcu poprzedniej lekcji. To był ostatni krok tej lekcji. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie przycisk Dalej, a za nim następna lekcja. Statek już nie ucieka z okna, ale wciąż nic z niego nie leci. Na następnej lekcji naciśniesz spację i to się zmieni.
Twoja gra ma już granice, ale nic w niej nie leci do przodu. Na tej lekcji naciśniesz spację i ze statku wyleci pocisk. Zanim jednak napiszesz choć jedną linię, trzeba rozstrzygnąć, jak spacja ma działać, bo są dwa zupełnie różne sposoby, w jakie klawisz może coś zrobić.
Pomyśl o pedale gazu i o dzwonku do drzwi. Pedał działa tak długo, jak go trzymasz: naciśnięty, samochód jedzie; puszczony, przestaje. Dzwonek działa inaczej. Naciskasz i słychać jeden dzwonek. Możesz trzymać palec na przycisku przez pół minuty, a dzwonek i tak zadzwoni raz. Strzałki w Twojej grze to pedał. W każdej z sześćdziesięciu klatek na sekundę program pyta klawiaturę, czy strzałka jest wciśnięta, i jeśli tak, przesuwa statek o sześć pikseli. Dopóki trzymasz, statek płynie.
Teraz pytanie. Gdyby spacja działała tak samo jak strzałki, a Ty przytrzymasz ją przez sekundę, ile pocisków wyleci ze statku? Wybierz odpowiedź, zanim czytasz dalej.
Sześćdziesiąt, bo pytanie o wciśnięty klawisz pada w każdej klatce i w każdej z nich odpowiedź brzmi „tak". Przez sekundę program strzeliłby sześćdziesiąt razy. Dla strzałek to dobrze, bo statek ma płynąć. Dla strzału to byłaby katastrofa: jedno naciśnięcie i ekran pełen pocisków. Spacja musi być dzwonkiem, nie pedałem. Jedno naciśnięcie, jeden pocisk, choćby trzymać ją pół minuty. A dzwonek w Twoim programie już jest, tylko dotąd dzwonił na coś innego.
Na drugiej lekcji Twój program dostał skrzynkę ze zdarzeniami. Pygame wrzuca do niej karteczkę za każdym razem, gdy coś się wydarzy, a pętla w każdym obiegu czyta wszystkie karteczki po kolei. Dotąd obchodziła Cię jedna: krzyżyk okna. Tak wygląda ta skrzynka w Twoim pliku:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
Krzyżyk jest dzwonkiem: jedno kliknięcie, jedna karteczka. Naciśnięcie klawisza też jest dzwonkiem i też zostawia w skrzynce karteczkę, dokładnie jedną, choćby klawisz był trzymany. Ta karteczka ma swój typ, tak jak krzyżyk ma pygame.QUIT, i sprawdzasz ją tym samym wzorem: if zdarzenie.type == pygame. i nazwa typu. Jest jednak różnica. Karteczka krzyżyka nie musi mówić nic więcej, bo krzyżyk jest jeden. Karteczka naciśnięcia klawisza musi jeszcze powiedzieć, który klawisz naciśnięto, i nosi to w polu zdarzenie.key. Nazwa spacji w tym polu idzie tym samym wzorem, co nazwy strzałek z ósmej lekcji, gdzie K_LEFT oznaczało strzałkę w lewo.
Jedna rzecz o miejscu. Pytanie o klawisz zadajesz wewnątrz sprawdzenia typu, jako drugi if wcięty pod pierwszym. Nie wszystkie karteczki mają klawisz: karteczka ruchu myszy nie ma go wcale, więc pytanie o zdarzenie.key zadane każdej karteczce po kolei skończyłoby się błędem. Najpierw „czy to naciśnięcie klawisza", dopiero potem „którego". Uzupełnij dwie luki. Sprawdzarka wrzuci do skrzynki cztery karteczki, jedną po drugiej, i po każdej policzy, ile było strzałów.
Cztery karteczki, jeden strzał. Ruch myszy nie przeszedł przez pierwszy if. Strzałka w prawo przeszła przez pierwszy, bo to naciśnięcie klawisza, ale nie przez drugi, bo to nie spacja. Puszczenie spacji to osobna karteczka innego typu, więc też nie policzyła się jako strzał. Został jeden: naciśnięcie spacji. Od tej chwili Twój program umie odróżnić spację od wszystkiego innego, co wpada do skrzynki. Nie umie jeszcze tylko jednego: strzelić.
Pocisk to prostokąt, cztery piksele szeroki i osiemnaście wysoki. Nie ma obrazka, więc nie potrzebuje karty jak statek; wystarczy sama ramka, taka jak ta, która na poprzedniej lekcji powstała z czterech liczb. Nowy pocisk ma się urodzić na dziobie statku: w jego osi, czyli środkiem na środku statku, i tuż nad nim, tak żeby dół pocisku dotykał góry statku. Gotowy pocisk trafia na listę pocisków, bo za chwilę będzie ich wiele.
Ułóż ciało funkcji wystrzel z klocków. Klocków jest pięć, a do funkcji wchodzą cztery; ten, który nie pasuje, odłóż przyciskiem na bok. Kolejność klocków to kolejność zdań: najpierw prostokąt musi w ogóle istnieć, potem można go ustawić, a do listy trafia dopiero gotowy.
Zwróć uwagę, że odłożony klocek nie był błędny. Python przyjąłby go bez słowa i pocisk urodziłby się w innym miejscu, tylko nie tam, gdzie dziób. Czytaj uchwyty ramki uważnie: top i bottom to dwie różne krawędzie i pomylenie ich nie daje żadnego komunikatu, tylko pocisk w złym miejscu.
Pociski są na liście, ale lista sama się nie rysuje. Ten ruch już znasz z siódmej lekcji: gwiazdy też są listą, a rysuj_tlo przechodzi po niej pętlą i każdej gwieździe daje jedną kropkę. Z pociskami jest tak samo. Lista wchodzi do pętli, każdy element dostaje jeden prostokąt narysowany przez pygame.draw.rect, które znasz z poprzedniej lekcji. Kolor (255, 240, 200) to jasna, ciepła biel, żeby pocisk było widać na czarnym niebie.
Uzupełnij dwie luki. Pierwsza to nazwa, pod którą pętla podaje Ci kolejny element listy, druga to nazwa tego, co ma zostać narysowane. Ten krok nie wypisze liczby, tylko narysuje obrazek: małą planszę, nie okno gry, z dwoma pociskami z listy. Jeśli widzisz na niej dwa jasne prostokąty, pętla działa.
Obie luki to jedno i to samo słowo, bo to, co pętla wyjmuje z listy, jest dokładnie tym, co rysujesz. Pętla po dwóch pociskach narysuje dwa, po dwudziestu narysuje dwadzieścia. Nie musisz wiedzieć z góry, ile ich będzie, i to jest cała siła listy.
wystrzelW trzecim kroku ciało tej funkcji było ułożone z klocków. Teraz napisz je własną ręką. Funkcja nazywa się wystrzel(pociski, statek), dostaje listę pocisków i kartę statku, a ma zrobić cztery rzeczy: zbudować prostokąt pocisku o rozmiarze cztery na osiemnaście, ustawić go środkiem na środku statku, ustawić jego dół na górze statku i dołożyć do listy. Sprawdzarka strzeli raz i zajrzy na listę: ma tam być jeden pocisk, właściwego rozmiaru, w osi statku i dotykający jego dziobu.
Zauważ, że wystrzel niczego nie oddaje. Dokłada pocisk do listy, którą dostała, i to wystarczy, bo lista jest jedna: ta sama, którą za chwilę stworzysz w main. Gdy funkcja dołoży coś do tej listy, main zobaczy to od razu, bez żadnego przekazywania z powrotem.
Tak wygląda gotowa funkcja. Wpisz ją do swojego pliku po steruj_statkiem, a przed rysuj_tlo:
def wystrzel(pociski, statek):
pocisk = pygame.Rect(0, 0, 4, 18)
pocisk.centerx = statek["prostokat"].centerx
pocisk.bottom = statek["prostokat"].top
pociski.append(pocisk)
Sama funkcja nie zmieni jeszcze niczego, bo nikt jej nie woła i nie ma listy, do której miałaby dokładać. Dlatego plik rośnie w tym kroku jeszcze w trzech miejscach, wszystkie w main. Pierwsze: pusta lista pocisków, tuż po karcie statku, przed gramy = True:
statek = nowy_statek(obraz_statku)
pociski = []
Drugie: w skrzynce ze zdarzeniami, pod warunkiem na krzyżyk i na tym samym wcięciu, ten sam podwójny if z drugiego kroku. Tym razem zamiast liczyć strzały, woła wystrzel:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_SPACE:
wystrzel(pociski, statek)
Trzecie: w szufladzie MALUJ, pętla z czwartego kroku. Jej miejsce ma znaczenie: między tłem a statkiem. To, co rysujesz później, ląduje na wierzchu, więc pociski malowane przed statkiem będą pod nim, a statek zostanie na wierzchu:
rysuj_tlo(ekran, gwiazdy)
for pocisk in pociski:
pygame.draw.rect(ekran, (255, 240, 200), pocisk)
ekran.blit(statek["obraz"], statek["prostokat"])
Zanim klikniesz, zapamiętaj plan: klikasz w okno gry, naciskasz spację jeden raz i patrzysz na dziób statku. Kliknij ▶ Uruchom, kliknij w okno gry i naciśnij spację jeden raz.
Na dziobie statku pojawił się jasny, wąski prostokąt i został tam, gdzie się urodził. Jedno naciśnięcie, jedna karteczka, jeden pocisk: to wystrzel dołożyła go do listy, a pętla w MALUJ narysowała. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie już przycisk Dalej.
import random
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
CZARNY = (26, 28, 64)
STATEK = [
".......11.......",
".......44.......",
"......1441......",
"......1331......",
".....143341.....",
".....143341.....",
"....11433411....",
"...1143333411...",
"..114333333411..",
".11433355334411.",
"1143333553333411",
"1433333553333341",
"1133311551333311",
"..11.1677761.11.",
"......167761....",
".......1771.....",
]
KOLORY = {
"1": (18, 22, 48),
"3": (150, 170, 210),
"4": (225, 240, 255),
"5": (0, 200, 255),
"6": (255, 150, 40),
"7": (255, 230, 120),
}
def zbuduj_statek(ksztalt, kolory, skala):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
for y in range(wysokosc):
for x in range(szerokosc):
znak = ksztalt[y][x]
if znak in kolory:
obraz.set_at((x, y), kolory[znak])
return pygame.transform.scale(obraz,
(szerokosc * skala, wysokosc * skala))
def losuj_gwiazdy(ile):
gwiazdy = []
for numer in range(ile):
x = random.randint(0, SZEROKOSC)
y = random.randint(0, WYSOKOSC)
jasnosc = random.choice([70, 110, 160, 210])
gwiazdy.append((x, y, jasnosc))
return gwiazdy
def nowy_statek(obraz):
prostokat = obraz.get_rect()
prostokat.centerx = SZEROKOSC // 2
prostokat.bottom = WYSOKOSC - 20
return {"obraz": obraz, "prostokat": prostokat, "predkosc": 6}
def steruj_statkiem(statek, klawisze):
prostokat = statek["prostokat"]
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
if klawisze[pygame.K_RIGHT]:
prostokat.x += statek["predkosc"]
if klawisze[pygame.K_UP]:
prostokat.y -= statek["predkosc"]
if klawisze[pygame.K_DOWN]:
prostokat.y += statek["predkosc"]
prostokat.clamp_ip(pygame.Rect(0, 0, SZEROKOSC, WYSOKOSC))
def wystrzel(pociski, statek):
pocisk = pygame.Rect(0, 0, 4, 18)
pocisk.centerx = statek["prostokat"].centerx
pocisk.bottom = statek["prostokat"].top
pociski.append(pocisk)
def rysuj_tlo(ekran, gwiazdy):
ekran.fill(CZARNY)
for x, y, jasnosc in gwiazdy:
pygame.draw.circle(ekran, (jasnosc, jasnosc, jasnosc + 20), (x, y), 1)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
gwiazdy = losuj_gwiazdy(90)
statek = nowy_statek(obraz_statku)
pociski = []
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_SPACE:
wystrzel(pociski, statek)
steruj_statkiem(statek, pygame.key.get_pressed())
rysuj_tlo(ekran, gwiazdy)
for pocisk in pociski:
pygame.draw.rect(ekran, (255, 240, 200), pocisk)
ekran.blit(statek["obraz"], statek["prostokat"])
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Czas sprawdzić dzwonek do końca. Plan: klikasz w okno gry, naciskasz spację kilka razy, jedno naciśnięcie po drugim, a na koniec naciskasz ją raz i przytrzymujesz przez dobrą sekundę. Po każdym pojedynczym naciśnięciu ma przybyć jeden pocisk. Po przytrzymaniu też ma przybyć jeden, nie sześćdziesiąt. Popatrz też, gdzie te pociski są, gdy odpłyniesz statkiem w bok. Kliknij ▶ Uruchom, kliknij w okno gry i naciśnij spację kilka razy, a potem raz ją przytrzymaj.
Pociski przybywają po jednym i wiszą w miejscu, w którym się urodziły. Statek odpływa, a one zostają, bo nic w Twoim pliku nie zmienia ich położenia po urodzeniu. To nie jest błąd, tylko koniec tej lekcji: spacja działa jak dzwonek i to było jej zadanie. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie przycisk Dalej, a za nim następna lekcja, na której te pociski wreszcie polecą.
Pociski w Twojej grze wiszą na dziobie, bo nic w pliku nie zmienia ich położenia po urodzeniu. Na tej lekcji polecą. Ruch będzie prosty: w każdej klatce każdy pocisk przesunie się o dwanaście pikseli w górę, dokładnie tak, jak statek przesuwa się o sześć na strzałkę. Dwanaście pikseli brzmi jak niewiele. Zanim jednak wpiszesz tę liczbę do pliku, warto wiedzieć, ile to naprawdę jest.
Program robi sześćdziesiąt klatek na sekundę. Ile pikseli przeleci pocisk w ciągu jednej sekundy? A okno ma sześćset pikseli wysokości: ile klatek potrzebuje pocisk, żeby przelecieć je całe, od dołu do góry? Policz w głowie, a potem kliknij Uruchom pomiar i sprawdź.
Pocisk przelatuje całe okno w mniej niż sekundę. Sekundę po strzale nie ma go już w oknie. Jest za to jedno pytanie, na które ten rachunek nie odpowiada: co się z tym pociskiem wtedy dzieje. Okno kończy się na górnej krawędzi, ale Twój plik o tej krawędzi nic nie wie. O tym jest następny krok.
Ktoś, kto buduje tę samą grę, dopisał ruch i nic więcej. W szufladzie LICZ przechodzi pętlą po liście pocisków i każdemu odejmuje dwanaście od y. Uruchamia grę i wszystko wygląda dobrze: naciska spację, pocisk leci w górę, dociera do krawędzi i znika. Kolejny strzał, kolejny pocisk, to samo. Gra działa. Można grać pół godziny i nie zobaczyć niczego dziwnego.
Pytanie nie jest o to, co widać w oknie, bo w oknie wszystko jest w porządku. Pytanie jest o listę. Pomyśl, co dzieje się z listą pocisków po każdym strzale i co dzieje się z pociskiem, który przeleciał krawędź. Potem wybierz odpowiedź.
Pocisk za krawędzią nie znika naprawdę. Znika z okna, bo Pygame nie rysuje niczego poza nim, ale na liście zostaje i w każdej klatce wciąż dostaje swoje dwanaście pikseli w górę. Po stu strzałach lista ma sto pocisków, po tysiącu tysiąc, i każdy z nich w każdej klatce jest przesuwany i rysowany w miejscu, którego nikt nie widzi. To jest pomyłka, której nie widać: gra wygląda dobrze, a robi się coraz cięższa. Potrzebne jest coś, co wyrzuci z listy pociski, których już nie ma w oknie.
Python ma na to jeden wiersz. Wygląda tak: zostaja = [p for p in pociski if warunek]. Czytaj go od środka. Lista pociski wchodzi, pętla wyjmuje z niej po jednym elemencie i nazywa go p, każdy element jest sprawdzany warunkiem, a te, które przeszły, trafiają do nowej listy. Ta nowa lista wychodzi z prawej strony i ląduje w zostaja. W tej lekcji nazywamy ten wiersz sitem: sypiesz na nie całą listę, a przez oczka przechodzi tylko to, co spełnia warunek. Jedna rzecz jest ważna: stara lista zostaje nietknięta. Sito niczego z niej nie wyrzuca, tylko buduje nową, mniejszą.
Zostaje ustalić warunek. Masz trzy pociski: jeden w oknie, jeden w całości nad górną krawędzią i jeden, który wystaje z okna tylko kawałkiem, bo jego dół jest jeszcze poniżej krawędzi. Górna krawędź okna to zero. Pocisk jest widoczny, dopóki jego dół jest poniżej tej krawędzi, czyli dopóki wartość bottom jest większa od zera. Uzupełnij lukę w sicie tak, żeby przeszły przez nie dokładnie te pociski, które jeszcze widać. Program wypisze doły pocisków, które zostały.
Zostały dwa: ten w oknie i ten, który wystaje kawałkiem. Wystający przeszedł przez sito, bo jego dół jest wciąż poniżej krawędzi, więc kilka ostatnich pikseli tego pocisku jest jeszcze na ekranie. Wypadł tylko ten, który jest cały nad oknem. Sito nie ocenia, czy pocisk jest ładny albo potrzebny; sprawdza jeden warunek i nic więcej.
rusz_pociskiTeraz połącz ruch z sitem w jednej funkcji. Nazywa się rusz_pociski(pociski), dostaje listę pocisków i ma zrobić dwie rzeczy: przejść pętlą po liście i każdy pocisk przesunąć o dwanaście pikseli w górę, a potem oddać przez return nową listę przepuszczoną przez sito z poprzedniego kroku. Sprawdzarka poda te same trzy pociski, co przed chwilą, i sprawdzi, co wróciło.
Kolejność ma znaczenie i to jest jedyna rzecz, którą trzeba tu przemyśleć. Najpierw ruch, potem sito. Sito pyta, gdzie pocisk jest, a nie gdzie był przed chwilą. Jeśli przesiejesz listę przed ruchem, pocisk, który po tym ruchu wyleci cały za krawędź, przejdzie przez sito i zostanie na liście na jedną klatkę dłużej, niż trzeba. Ruch pierwszy, sito na końcu, jako ostatnie zdanie funkcji.
Wróciły dwa pociski. Ten z okna jest o dwanaście wyżej, ten wystający kawałkiem ma dół na jedynce, czyli w tej klatce widać go jeszcze jednym pikselem, a w następnej wypadnie z sita. Trzeci, który przed ruchem był cały nad oknem, teraz jest jeszcze wyżej i przez sito nie przeszedł. Tak wygląda gotowa funkcja. Wpisz ją do pliku po wystrzel, a przed rysuj_tlo:
def rusz_pociski(pociski):
for pocisk in pociski:
pocisk.y -= 12
return [p for p in pociski if p.bottom > 0]
Sama funkcja nie ruszy jeszcze niczego, bo nikt jej nie woła. Plik rośnie w tym kroku w jeszcze jednym miejscu: w szufladzie LICZ, zaraz pod sterowaniem statku. Funkcja oddaje nową listę, więc ktoś musi ją odebrać i to robi pociski = z przodu. Od tej linii pociski w main znaczy „lista po ruchu i po sicie":
steruj_statkiem(statek, pygame.key.get_pressed())
pociski = rusz_pociski(pociski)
Zanim klikniesz, zapamiętaj plan: klikasz w okno gry, naciskasz spację jeden raz i patrzysz, jak pocisk leci od dziobu do górnej krawędzi. Kliknij ▶ Uruchom, kliknij w okno gry i naciśnij spację jeden raz.
Pocisk wystartował z dziobu, przeleciał okno w górę w mniej niż sekundę i zniknął za krawędzią. W każdej klatce rusz_pociski przesunęła go o dwanaście pikseli, a gdy jego dół minął zero, sito zostawiło go poza nową listą. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie już przycisk Dalej.
import random
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
CZARNY = (26, 28, 64)
STATEK = [
".......11.......",
".......44.......",
"......1441......",
"......1331......",
".....143341.....",
".....143341.....",
"....11433411....",
"...1143333411...",
"..114333333411..",
".11433355334411.",
"1143333553333411",
"1433333553333341",
"1133311551333311",
"..11.1677761.11.",
"......167761....",
".......1771.....",
]
KOLORY = {
"1": (18, 22, 48),
"3": (150, 170, 210),
"4": (225, 240, 255),
"5": (0, 200, 255),
"6": (255, 150, 40),
"7": (255, 230, 120),
}
def zbuduj_statek(ksztalt, kolory, skala):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
for y in range(wysokosc):
for x in range(szerokosc):
znak = ksztalt[y][x]
if znak in kolory:
obraz.set_at((x, y), kolory[znak])
return pygame.transform.scale(obraz,
(szerokosc * skala, wysokosc * skala))
def losuj_gwiazdy(ile):
gwiazdy = []
for numer in range(ile):
x = random.randint(0, SZEROKOSC)
y = random.randint(0, WYSOKOSC)
jasnosc = random.choice([70, 110, 160, 210])
gwiazdy.append((x, y, jasnosc))
return gwiazdy
def nowy_statek(obraz):
prostokat = obraz.get_rect()
prostokat.centerx = SZEROKOSC // 2
prostokat.bottom = WYSOKOSC - 20
return {"obraz": obraz, "prostokat": prostokat, "predkosc": 6}
def steruj_statkiem(statek, klawisze):
prostokat = statek["prostokat"]
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
if klawisze[pygame.K_RIGHT]:
prostokat.x += statek["predkosc"]
if klawisze[pygame.K_UP]:
prostokat.y -= statek["predkosc"]
if klawisze[pygame.K_DOWN]:
prostokat.y += statek["predkosc"]
prostokat.clamp_ip(pygame.Rect(0, 0, SZEROKOSC, WYSOKOSC))
def wystrzel(pociski, statek):
pocisk = pygame.Rect(0, 0, 4, 18)
pocisk.centerx = statek["prostokat"].centerx
pocisk.bottom = statek["prostokat"].top
pociski.append(pocisk)
def rusz_pociski(pociski):
for pocisk in pociski:
pocisk.y -= 12
return [p for p in pociski if p.bottom > 0]
def rysuj_tlo(ekran, gwiazdy):
ekran.fill(CZARNY)
for x, y, jasnosc in gwiazdy:
pygame.draw.circle(ekran, (jasnosc, jasnosc, jasnosc + 20), (x, y), 1)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
gwiazdy = losuj_gwiazdy(90)
statek = nowy_statek(obraz_statku)
pociski = []
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_SPACE:
wystrzel(pociski, statek)
steruj_statkiem(statek, pygame.key.get_pressed())
pociski = rusz_pociski(pociski)
rysuj_tlo(ekran, gwiazdy)
for pocisk in pociski:
pygame.draw.rect(ekran, (255, 240, 200), pocisk)
ekran.blit(statek["obraz"], statek["prostokat"])
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Ktoś inny napisał rusz_pociski dokładnie tak, jak Ty, ale w szufladzie LICZ wpisał samo rusz_pociski(pociski), bez niczego z przodu. Tak samo, jak woła się wystrzel(pociski, statek), które przecież działa. Zanim odpowiesz, przypomnij sobie siódmą lekcję. Funkcja losuj_gwiazdy(90) oddawała listę gwiazd, a w pliku wołasz ją linią gwiazdy = losuj_gwiazdy(90). Pomyśl, co by się stało, gdyby z tamtej linii wyciąć gwiazdy =. Potem pomyśl, co ta osoba zobaczy w swojej grze, i wybierz odpowiedź.
Pociski lecą, ale lista nigdy się nie skraca. Ruch działa, bo pętla w rusz_pociski zmienia prostokąty na tej samej liście, którą dostała, a lista jest jedna. Sito też działa: buduje nową, krótszą listę i oddaje ją przez return. Tylko że tej nowej listy nikt nie odebrał, więc przepada, a main dalej trzyma starą, w której są wszystkie pociski od pierwszego strzału. To druga pomyłka na tej lekcji, której nie widać: gra wygląda dobrze i robi się coraz cięższa. Dlatego wystrzel woła się samym imieniem, bo dokłada do listy, którą dostała, a rusz_pociski z pociski = z przodu, bo oddaje nową.
Twoja gra ma teraz ruch w dwóch kierunkach: statek płynie na boki, pociski lecą w górę. Sprawdź jedno i drugie naraz. Plan: klikasz w okno gry, strzelasz kilka razy bez wciśniętej strzałki, a potem przytrzymujesz strzałkę w bok i strzelasz w ruchu. Każdy pocisk ma wystartować z miejsca, w którym w tej chwili jest dziób, i lecieć prosto w górę, choć statek już odpłynął. Kliknij ▶ Uruchom, kliknij w okno gry i wystrzel kilka pocisków, także z przytrzymaną strzałką.
Pociski lecą, znikają za krawędzią i nie zostają po nich żadne ślady na liście. Twoja gra ma statek, granice, spację i ruch, a nie ma w co trafić. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie przycisk Dalej, a za nim następna lekcja, na której do Twojego pliku wjadą pierwsze obrazki z dysku.
Statek strzela, pociski lecą, a nie ma w co trafić. Kamienie spadną na następnej lekcji. Na tej trzeba je najpierw dostać do gry, a one nie są rysowane kropkami jak statek. Leżą na dysku jako gotowe obrazki, w katalogu assety obok Twojego pliku gry. Jest ich trzynaście. Dziewięć asteroid w trzech rozmiarach, po trzy w każdym: asteroida_duza_1.png, asteroida_duza_2.png, asteroida_duza_3.png, potem to samo dla srednia i mala. Do tego cztery klatki wybuchu, od wybuch_1.png do wybuch_4.png.
Trzynaście plików, a nikt nie będzie pisał trzynastu linii. Przyjrzyj się nazwom asteroid: różnią się tylko rozmiarem i numerem, a rozmiary są trzy i numery są trzy. Wzór w nazwach to pętla. Pętla po rozmiarach, w niej pętla po numerach, a w samym środku jedno wczytanie. Zanim to napiszesz, odpowiedz: ile razy wykona się to jedno wczytanie w środku?
Zewnętrzna pętla obraca się trzy razy, a przy każdym jej obrocie wewnętrzna obraca się trzy razy. Trzy razy trzy, dziewięć wczytań z jednej linii. Cztery klatki wybuchu to osobna, pojedyncza pętla. Razem trzynaście obrazków i tylko dwa miejsca w kodzie, w których cokolwiek jest wczytywane. Zostaje jedno: powiedzieć programowi, gdzie te pliki leżą.
Program nie zgadnie, gdzie jest obrazek. Trzeba mu podać ścieżkę, czyli drogę do pliku. Droga składa się z katalogu, ukośnika i nazwy pliku: assety/asteroida_duza_2.png. Jest liczona od miejsca, w którym leży plik gry, dlatego katalog assety musi leżeć obok gra.py. Jeśli przeniesiesz katalog gdzie indziej, ścieżka nie zmieni się ani o literę, a plik przestanie być pod nią do znalezienia.
Nazwę katalogu trzymasz w stałej KATALOG, a resztę ścieżki składasz f-stringiem, tak samo, jak w Podstawach składało się napisy ze zmiennych. W klamrach f-stringu nie wpisuje się słów z nazwy pliku, tylko nazwy zmiennych, które te słowa trzymają. Uzupełnij dwie luki tak, żeby dla rozmiaru duza i numeru 2 wyszła ścieżka do drugiej dużej asteroidy.
Ten sam f-string z innymi wartościami w zmiennych da inną ścieżkę, i to jest cały pomysł: w pętli po rozmiarach i numerach jedna linia buduje dziewięć różnych ścieżek, po jednej na obrót.
Ścieżka jest, teraz obrazek. Pygame ma w swojej szufladzie pygame.image narzędzie do wczytywania obrazka z pliku. Jego nazwa to angielskie „wczytaj", cztery litery. Podajesz mu ścieżkę, a ono oddaje kartkę. Nie jakąś nową rzecz: dokładnie taką samą kartkę, jaką na piątej lekcji budowała funkcja zbuduj_statek z kropek. Ma rozmiar, ma piksele, można ją położyć na ekranie i zapytać o ramkę. Różnica jest tylko w tym, skąd się wzięła.
Uzupełnij lukę tak, żeby pierwsza duża asteroida została wczytana. Sprawdzarka zapyta obrazek o rozmiar i zajrzy do piksela na samym środku, żeby sprawdzić, czy to naprawdę szary kamień.
Osiemdziesiąt na osiemdziesiąt pikseli i szary środek: to jest duża asteroida. Program nie wie, że to kamień, i nie musi wiedzieć. Dla niego to kartka jak każda inna, tyle że dostał ją gotową z dysku.
Kartkę z pliku kładzie się na ekranie tym samym poleceniem, co kartkę z kropek. W main, w szufladzie MALUJ, jest linia, która kładzie obraz statku na jego prostokącie. To polecenie jest luką w tym kroku. Zajrzyj do tamtej linii w swoim pliku i użyj tego samego słowa.
Ten krok narysuje obrazek zamiast wypisać liczbę: małą planszę, na której pierwsza duża asteroida ma leżeć lewym górnym rogiem w punkcie (80, 0), czyli u samej góry. Uzupełnij lukę i sprawdź, czy na planszy widać szary kamień przyklejony do górnej krawędzi.
Jedna kartka z kropek, druga z pliku, a polecenie to samo. Pygame nie rozróżnia, skąd kartka pochodzi. Wszystko, co umiesz robić ze statkiem, umiesz od tej chwili robić z asteroidą.
wczytaj_obrazkiTrzynaście kartek trzeba gdzieś trzymać, a lista nie wystarczy, bo są cztery rodzaje. Tu wchodzi słownik list: pod kluczem "duza" leży lista trzech dużych asteroid, pod "srednia" trzy średnie, pod "mala" trzy małe, a pod "wybuch" cztery klatki wybuchu. Słownik znasz z karty statku, listę i append z pocisków. Nowość polega tylko na tym, że wartością w słowniku jest lista, więc obrazki["duza"] to lista, a obrazki["duza"][0] to jej pierwsza kartka.
Funkcja wczytaj_obrazki buduje ten słownik. Dostajesz ją z gotową podwójną pętlą po asteroidach, dokładnie tą, o którą pytał pierwszy krok. Brakuje w niej drugiej pętli: pojedynczej, po czterech numerach klatek wybuchu. Każdy obrót ma zbudować ścieżkę do pliku wybuch_<numer>.png, wczytać kartkę i dołożyć ją do listy pod kluczem "wybuch". Dopisz tę pętlę. Sprawdzarka zajrzy do słownika: cztery klucze, trzy duże asteroidy, cztery klatki wybuchu i właściwe rozmiary.
Tak wygląda cała funkcja. Wpisz ją do pliku po zbuduj_statek, a przed losuj_gwiazdy:
def wczytaj_obrazki():
obrazki = {"duza": [], "srednia": [], "mala": [], "wybuch": []}
for rozmiar in ("duza", "srednia", "mala"):
for numer in (1, 2, 3):
plik = f"{KATALOG}/asteroida_{rozmiar}_{numer}.png"
obrazki[rozmiar].append(pygame.image.load(plik))
for numer in (1, 2, 3, 4):
obrazki["wybuch"].append(
pygame.image.load(f"{KATALOG}/wybuch_{numer}.png"))
return obrazki
Plik rośnie w tym kroku w trzech miejscach. Pierwsze to stała z nazwą katalogu, u góry pliku, pod WYSOKOSC = 600:
KATALOG = "assety"
Drugie to sama funkcja, którą masz już wpisaną. Trzecie to dwie linie w main. Obrazki wczytuje się raz, na starcie, zanim cokolwiek z nich skorzysta, więc pierwsza linia jest zaraz po zegarze, przed budową statku. Funkcja oddaje słownik, więc obrazki = z przodu go odbiera, tak jak przy gwiazdach:
zegar = pygame.time.Clock()
obrazki = wczytaj_obrazki()
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
Druga linia to sonda: jedna duża asteroida położona u góry okna, w szufladzie MALUJ, zaraz pod tłem. Nie jest częścią gry, tylko dowodem, że wczytanie działa. Zniknie, gdy kamienie zaczną spadać:
rysuj_tlo(ekran, gwiazdy)
ekran.blit(obrazki["duza"][0], (360, 0))
Zanim klikniesz, zapamiętaj plan: patrzysz na samą górę okna, blisko środka, i szukasz szarego kamienia. Kliknij ▶ Uruchom i popatrz na górną krawędź okna.
U góry okna, blisko środka, pojawił się szary kamień: pierwsza z trzynastu kartek, które wczytaj_obrazki wzięła z dysku i włożyła do słownika. To ta sonda z MALUJ, a reszta obrazków czeka w słowniku na swoją kolej. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie już przycisk Dalej.
import random
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
KATALOG = "assety"
CZARNY = (26, 28, 64)
STATEK = [
".......11.......",
".......44.......",
"......1441......",
"......1331......",
".....143341.....",
".....143341.....",
"....11433411....",
"...1143333411...",
"..114333333411..",
".11433355334411.",
"1143333553333411",
"1433333553333341",
"1133311551333311",
"..11.1677761.11.",
"......167761....",
".......1771.....",
]
KOLORY = {
"1": (18, 22, 48),
"3": (150, 170, 210),
"4": (225, 240, 255),
"5": (0, 200, 255),
"6": (255, 150, 40),
"7": (255, 230, 120),
}
def zbuduj_statek(ksztalt, kolory, skala):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
for y in range(wysokosc):
for x in range(szerokosc):
znak = ksztalt[y][x]
if znak in kolory:
obraz.set_at((x, y), kolory[znak])
return pygame.transform.scale(obraz,
(szerokosc * skala, wysokosc * skala))
def wczytaj_obrazki():
obrazki = {"duza": [], "srednia": [], "mala": [], "wybuch": []}
for rozmiar in ("duza", "srednia", "mala"):
for numer in (1, 2, 3):
plik = f"{KATALOG}/asteroida_{rozmiar}_{numer}.png"
obrazki[rozmiar].append(pygame.image.load(plik))
for numer in (1, 2, 3, 4):
obrazki["wybuch"].append(
pygame.image.load(f"{KATALOG}/wybuch_{numer}.png"))
return obrazki
def losuj_gwiazdy(ile):
gwiazdy = []
for numer in range(ile):
x = random.randint(0, SZEROKOSC)
y = random.randint(0, WYSOKOSC)
jasnosc = random.choice([70, 110, 160, 210])
gwiazdy.append((x, y, jasnosc))
return gwiazdy
def nowy_statek(obraz):
prostokat = obraz.get_rect()
prostokat.centerx = SZEROKOSC // 2
prostokat.bottom = WYSOKOSC - 20
return {"obraz": obraz, "prostokat": prostokat, "predkosc": 6}
def steruj_statkiem(statek, klawisze):
prostokat = statek["prostokat"]
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
if klawisze[pygame.K_RIGHT]:
prostokat.x += statek["predkosc"]
if klawisze[pygame.K_UP]:
prostokat.y -= statek["predkosc"]
if klawisze[pygame.K_DOWN]:
prostokat.y += statek["predkosc"]
prostokat.clamp_ip(pygame.Rect(0, 0, SZEROKOSC, WYSOKOSC))
def wystrzel(pociski, statek):
pocisk = pygame.Rect(0, 0, 4, 18)
pocisk.centerx = statek["prostokat"].centerx
pocisk.bottom = statek["prostokat"].top
pociski.append(pocisk)
def rusz_pociski(pociski):
for pocisk in pociski:
pocisk.y -= 12
return [p for p in pociski if p.bottom > 0]
def rysuj_tlo(ekran, gwiazdy):
ekran.fill(CZARNY)
for x, y, jasnosc in gwiazdy:
pygame.draw.circle(ekran, (jasnosc, jasnosc, jasnosc + 20), (x, y), 1)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
obrazki = wczytaj_obrazki()
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
gwiazdy = losuj_gwiazdy(90)
statek = nowy_statek(obraz_statku)
pociski = []
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_SPACE:
wystrzel(pociski, statek)
steruj_statkiem(statek, pygame.key.get_pressed())
pociski = rusz_pociski(pociski)
rysuj_tlo(ekran, gwiazdy)
ekran.blit(obrazki["duza"][0], (360, 0))
for pocisk in pociski:
pygame.draw.rect(ekran, (255, 240, 200), pocisk)
ekran.blit(statek["obraz"], statek["prostokat"])
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Pora obejrzeć sondę w całej grze. Plan: klikasz w okno gry, płyniesz statkiem na boki, strzelasz kilka razy i cały czas obserwujesz kamień u góry. Statek płynie, pociski lecą, a kamień ma być w tym samym miejscu, przybity do górnej krawędzi. Kliknij ▶ Uruchom, kliknij w okno gry, popłyń i postrzelaj, patrząc na kamień u góry.
Kamień jest na swoim miejscu i nie rusza się, cokolwiek robi statek. Ta jedna linia w MALUJ mówi jedno: trzynaście obrazków wjechało do gry i można je kłaść na ekranie jak statek. To jest sonda, nie kamień, w który da się trafić; zniknie z pliku, gdy kamienie zaczną spadać. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie przycisk Dalej, a za nim ostatnie pytanie tej lekcji o to, co przed chwilą było widać.
Pętla Twojej gry obraca się sześćdziesiąt razy na sekundę i w każdym obrocie kładzie kamień na ekranie. Mimo to kamień nie przesunął się ani o piksel. Pociski w tej samej pętli lecą, statek płynie. Pomyśl, czym kamień różni się od nich w Twoim pliku, i wybierz odpowiedź.
Kamień nie spada, bo nic w pętli nie zmienia jego położenia. Pociski lecą, bo w szufladzie LICZ jest funkcja, która co klatkę odejmuje im dwanaście od y. Statek płynie, bo sterowanie co klatkę przesuwa jego prostokąt. Kamień nie ma ani prostokąta, ani funkcji, ani nawet listy: jest kładziony w tym samym punkcie w każdej klatce, więc w każdej klatce jest w tym samym miejscu. Na następnej lekcji kamienie dostaną własną listę i własną funkcję, tak jak pociski, a każdy będzie spadał inaczej.
Sonda u góry okna pokazała, że obrazki są w grze. Na tej lekcji kamienie zaczną spadać naprawdę: co chwilę nowy, w losowym miejscu, w losowym rozmiarze, z własną prędkością. Pierwsze pytanie brzmi: co ile ma się rodzić nowy kamień. Pętla obraca się sześćdziesiąt razy na sekundę, a kamień co obieg to byłaby lawina. Trzeba rodzić go co któryś obieg.
Do liczenia obiegów wystarczy zmienna licznik, która w każdej klatce rośnie o jeden. Do wybierania co któregoś obiegu wystarczy znak %, który znasz z Podstaw jako resztę z dzielenia. Reszta z dzielenia licznik przez trzydzieści jest zerem, gdy licznik wynosi trzydzieści, sześćdziesiąt, dziewięćdziesiąt i tak dalej. Warunek licznik % 30 == 0 jest więc prawdą dokładnie co trzydziesty obieg i wtedy rodzi się kamień. Zostaje jedno: przeliczyć obiegi na sekundy. Metronom bije sześćdziesiąt razy na sekundę. Co ile sekund pojawi się nowy kamień?
Co pół sekundy, czyli dwa kamienie na sekundę. Trzydzieści obiegów to połowa z sześćdziesięciu, które mieszczą się w sekundzie. Gdy zechcesz, żeby kamienie spadały rzadziej, wystarczy zmienić trzydzieści na większą liczbę, i to jest cała regulacja tempa gry: jedna liczba w jednym warunku.
Kamień ma trzy możliwe rozmiary: duży, średni i mały. Rozmiar będzie losowany przez random.choice, tak jak na siódmej lekcji losował się kolor gwiazdy. Lista, z której losuje, wygląda tak: ["duza", "duza", "srednia", "mala"]. Słowo "duza" jest na niej dwa razy, i to nie jest pomyłka.
Zanim sprawdzisz, co z tego wynika, obstaw. Program wylosuje z tej listy sto razy. Ile razy, według Ciebie, wypadnie duża? Wpisz swój typ w pole. Nie ma tu złej odpowiedzi, jest tylko Twoja przed pomiarem i pomiar po niej.
Teraz rozliczenie. Kod poniżej losuje z tej samej listy sto razy i liczy, ile razy wypadł każdy rozmiar. Zaczyna się od random.seed(7), które ustawia losowanie tak, żeby za każdym uruchomieniem dawało dokładnie te same wyniki. Dzięki temu liczby na ekranie to fakt, a nie przykład, i można je porównać z typem z poprzedniego kroku. Kliknij Uruchom pomiar i zobacz, jak blisko było.
Duża wypadła w około połowie losowań, a średnia i mała podzieliły się resztą. Tak działa random.choice: losuje miejsce na liście, nie słowo. Miejsca są cztery, każde równie prawdopodobne, a dwa z nich mówią „duza". Gdyby na liście była pojedyncza "duza", ten sam pomiar dałby trzydzieści osiem dużych, dwadzieścia siedem średnich i trzydzieści pięć małych, czyli mniej więcej po równo. Powtórzenie na liście to sposób, żeby los był nierówny, bez żadnego nowego narzędzia.
Dwie lekcje temu powstała funkcja rusz_pociski: pętla przesuwa każdy pocisk, a na końcu sito oddaje nową listę bez tych, które wyleciały za okno. Kamienie potrzebują dokładnie tego samego, tylko w drugą stronę. Napisz rusz_asteroidy(asteroidy) z pamięci, bez zaglądania do tamtej funkcji, jeśli dasz radę.
Dwie rzeczy są inne niż przy pociskach. Po pierwsze, kamień spada w dół, więc do y jego prostokąta się dodaje, nie odejmuje. Po drugie, każdy kamień ma własną prędkość, zapisaną w jego karcie pod kluczem "predkosc", tak jak statek ma swoją; nie ma jednej liczby dla wszystkich. Sito też odwraca się razem z kierunkiem: pocisk był widoczny, dopóki jego dół był poniżej górnej krawędzi, a kamień jest widoczny, dopóki jego góra jest powyżej dolnej krawędzi, czyli mniejsza od wysokości okna. Sprawdzarka poda trzy kamienie i zobaczy, które wrócą.
Wróciły dwa. Pierwszy przesunął się o swoją prędkość i jest daleko od dołu. Trzeci ma górę na 593, jeszcze siedem pikseli nad dolną krawędzią, więc wciąż go widać i przeszedł przez sito. Drugi po ruchu ma górę poniżej sześciuset i wypadł: cały jest już pod oknem. Sito nie patrzy, ile kamienia widać; pyta o jeden warunek i tyle.
Lista z podwójną dużą to nasz wybór, nie prawo. Ułóż własną. Może być więcej małych, żeby gra była szybka, może być po równo, może być siedem dużych na jedną małą. Program poniżej losuje z Twojej listy jeden rozmiar i go wypisuje. Kurs sprawdza tylko granice: każdy z trzech rozmiarów jest na liście przynajmniej raz, wpisów jest od trzech do dziesięciu, a nazwy to dokładnie "duza", "srednia" i "mala".
Ta lista trafi do Twojej funkcji w następnym kroku, więc to nie jest ćwiczenie na boku, tylko decyzja o Twojej grze. Uruchom kilka razy i zobacz, czy los zachowuje się tak, jak chcesz.
losuj_asteroideKamień, tak jak statek, potrzebuje karty: słownika, w którym leży wszystko, co gra musi o nim wiedzieć. Karta kamienia to karta statku plus jedno pole. Cztery klucze: "obraz", "prostokat", "predkosc" i nowy "rozmiar". Rozmiar nie jest potrzebny do spadania, ale za kilka lekcji, przy punktach, gra będzie chciała wiedzieć, jaki kamień został trafiony, i wtedy to pole się przyda.
Napisz losuj_asteroide(obrazki), funkcję, która buduje jedną taką kartę i ją oddaje. Po kolei: wylosuj rozmiar z listy z podwójną dużą, wylosuj obrazek z listy tego rozmiaru w słowniku obrazki, weź z obrazka jego prostokąt. Ustaw dół prostokąta na zerze, żeby kamień zaczynał cały nad oknem i wjeżdżał do niego od góry. Wylosuj x tak, żeby cały kamień mieścił się w oknie: najmniejsze x to zero, a największe to szerokość okna pomniejszona o szerokość kamienia. Prędkość wylosuj od jednego do trzech. Sprawdzarka wylosuje pięćdziesiąt kamieni i sprawdzi je wszystkie naraz, więc każde z tych ograniczeń musi działać za każdym razem, nie tylko przy szczęśliwym losie.
Tak wygląda gotowa funkcja. Wpisz ją do pliku po nowy_statek, a przed steruj_statkiem. Lista rozmiarów w pierwszej linii to nasza, z podwójną dużą; jeśli wolisz własne ważenie z poprzedniego kroku, wstaw tam swoją listę:
def losuj_asteroide(obrazki):
rozmiar = random.choice(["duza", "duza", "srednia", "mala"])
obraz = random.choice(obrazki[rozmiar])
prostokat = obraz.get_rect()
prostokat.x = random.randint(0, SZEROKOSC - prostokat.width)
prostokat.bottom = 0
return {"obraz": obraz, "prostokat": prostokat, "rozmiar": rozmiar,
"predkosc": random.randint(1, 3)}
Plik rośnie w tym kroku w czterech miejscach. Pierwsze to ta funkcja. Drugie to rusz_asteroidy z czwartego kroku, po rusz_pociski, a przed rysuj_tlo:
def rusz_asteroidy(asteroidy):
for asteroida in asteroidy:
asteroida["prostokat"].y += asteroida["predkosc"]
return [a for a in asteroidy if a["prostokat"].top < WYSOKOSC]
Trzecie miejsce to main przed pętlą: pusta lista kamieni i licznik obiegów, zaraz po pustej liście pocisków:
pociski = []
asteroidy = []
licznik = 0
Czwarte miejsce to szuflada LICZ. Po sterowaniu statku licznik rośnie o jeden, co trzydziesty obieg do listy trafia nowy kamień, a pod ruchem pocisków rusza się kamienie, z asteroidy = z przodu, bo funkcja oddaje nową listę:
steruj_statkiem(statek, pygame.key.get_pressed())
licznik += 1
if licznik % 30 == 0:
asteroidy.append(losuj_asteroide(obrazki))
pociski = rusz_pociski(pociski)
asteroidy = rusz_asteroidy(asteroidy)
Na koniec szuflada MALUJ. Sonda z poprzedniej lekcji zrobiła swoje i znika. W jej miejsce, zaraz pod tłem, wchodzi pętla po kamieniach, która każdy kładzie na ekranie jego obrazem na jego prostokącie, tak jak statek. Usuń linię sondy i wpisz pętlę:
rysuj_tlo(ekran, gwiazdy)
for asteroida in asteroidy:
ekran.blit(asteroida["obraz"], asteroida["prostokat"])
for pocisk in pociski:
pygame.draw.rect(ekran, (255, 240, 200), pocisk)
Zanim klikniesz, zapamiętaj plan: nie dotykasz klawiatury, tylko patrzysz na górną krawędź okna i liczysz, ile kamieni pojawi się w ciągu kilku sekund. Kliknij ▶ Uruchom i popatrz na górę okna.
Od górnej krawędzi wjeżdżają kamienie, mniej więcej dwa na sekundę, w różnych miejscach i rozmiarach, każdy w swoim tempie. To losuj_asteroide rodzi je co trzydziesty obieg, a rusz_asteroidy spuszcza w dół i przesiewa te, które wypadły pod okno. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie już przycisk Dalej.
import random
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
KATALOG = "assety"
CZARNY = (26, 28, 64)
STATEK = [
".......11.......",
".......44.......",
"......1441......",
"......1331......",
".....143341.....",
".....143341.....",
"....11433411....",
"...1143333411...",
"..114333333411..",
".11433355334411.",
"1143333553333411",
"1433333553333341",
"1133311551333311",
"..11.1677761.11.",
"......167761....",
".......1771.....",
]
KOLORY = {
"1": (18, 22, 48),
"3": (150, 170, 210),
"4": (225, 240, 255),
"5": (0, 200, 255),
"6": (255, 150, 40),
"7": (255, 230, 120),
}
def zbuduj_statek(ksztalt, kolory, skala):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
for y in range(wysokosc):
for x in range(szerokosc):
znak = ksztalt[y][x]
if znak in kolory:
obraz.set_at((x, y), kolory[znak])
return pygame.transform.scale(obraz,
(szerokosc * skala, wysokosc * skala))
def wczytaj_obrazki():
obrazki = {"duza": [], "srednia": [], "mala": [], "wybuch": []}
for rozmiar in ("duza", "srednia", "mala"):
for numer in (1, 2, 3):
plik = f"{KATALOG}/asteroida_{rozmiar}_{numer}.png"
obrazki[rozmiar].append(pygame.image.load(plik))
for numer in (1, 2, 3, 4):
obrazki["wybuch"].append(
pygame.image.load(f"{KATALOG}/wybuch_{numer}.png"))
return obrazki
def losuj_gwiazdy(ile):
gwiazdy = []
for numer in range(ile):
x = random.randint(0, SZEROKOSC)
y = random.randint(0, WYSOKOSC)
jasnosc = random.choice([70, 110, 160, 210])
gwiazdy.append((x, y, jasnosc))
return gwiazdy
def nowy_statek(obraz):
prostokat = obraz.get_rect()
prostokat.centerx = SZEROKOSC // 2
prostokat.bottom = WYSOKOSC - 20
return {"obraz": obraz, "prostokat": prostokat, "predkosc": 6}
def losuj_asteroide(obrazki):
rozmiar = random.choice(["duza", "duza", "srednia", "mala"])
obraz = random.choice(obrazki[rozmiar])
prostokat = obraz.get_rect()
prostokat.x = random.randint(0, SZEROKOSC - prostokat.width)
prostokat.bottom = 0
return {"obraz": obraz, "prostokat": prostokat, "rozmiar": rozmiar,
"predkosc": random.randint(1, 3)}
def steruj_statkiem(statek, klawisze):
prostokat = statek["prostokat"]
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
if klawisze[pygame.K_RIGHT]:
prostokat.x += statek["predkosc"]
if klawisze[pygame.K_UP]:
prostokat.y -= statek["predkosc"]
if klawisze[pygame.K_DOWN]:
prostokat.y += statek["predkosc"]
prostokat.clamp_ip(pygame.Rect(0, 0, SZEROKOSC, WYSOKOSC))
def wystrzel(pociski, statek):
pocisk = pygame.Rect(0, 0, 4, 18)
pocisk.centerx = statek["prostokat"].centerx
pocisk.bottom = statek["prostokat"].top
pociski.append(pocisk)
def rusz_pociski(pociski):
for pocisk in pociski:
pocisk.y -= 12
return [p for p in pociski if p.bottom > 0]
def rusz_asteroidy(asteroidy):
for asteroida in asteroidy:
asteroida["prostokat"].y += asteroida["predkosc"]
return [a for a in asteroidy if a["prostokat"].top < WYSOKOSC]
def rysuj_tlo(ekran, gwiazdy):
ekran.fill(CZARNY)
for x, y, jasnosc in gwiazdy:
pygame.draw.circle(ekran, (jasnosc, jasnosc, jasnosc + 20), (x, y), 1)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
obrazki = wczytaj_obrazki()
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
gwiazdy = losuj_gwiazdy(90)
statek = nowy_statek(obraz_statku)
pociski = []
asteroidy = []
licznik = 0
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_SPACE:
wystrzel(pociski, statek)
steruj_statkiem(statek, pygame.key.get_pressed())
licznik += 1
if licznik % 30 == 0:
asteroidy.append(losuj_asteroide(obrazki))
pociski = rusz_pociski(pociski)
asteroidy = rusz_asteroidy(asteroidy)
rysuj_tlo(ekran, gwiazdy)
for asteroida in asteroidy:
ekran.blit(asteroida["obraz"], asteroida["prostokat"])
for pocisk in pociski:
pygame.draw.rect(ekran, (255, 240, 200), pocisk)
ekran.blit(statek["obraz"], statek["prostokat"])
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Twoja gra ma teraz deszcz kamieni. Plan: klikasz w okno gry, przez chwilę tylko patrzysz, jak spadają, a potem latasz między nimi i strzelasz. Patrz uważnie na dwie rzeczy: co dzieje się, gdy kamień spada prosto na statek, i co dzieje się, gdy pocisk leci prosto w kamień. Kliknij ▶ Uruchom, kliknij w okno gry, popatrz chwilę, a potem polataj między kamieniami i postrzelaj.
Kamienie spadają przez statek i przez pociski, jakby ich nie było. Statek nie ginie, kamień nie pęka, pocisk leci dalej. Nic w Twoim pliku nie pyta, czy dwie ramki na siebie nachodzą, więc dla programu każdy kamień, każdy pocisk i statek żyją w osobnych światach. O tym pytaniu jest następna lekcja. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie przycisk Dalej.
Kamienie spadają przez pociski, bo nic w Twoim pliku nie pyta, czy dwie ramki na siebie nachodzą. Na tej lekcji to pytanie padnie, i to w każdej klatce, dla każdej pary pocisk i kamień. Zanim zada je program, zadaj je sobie, bo odpowiedź jest mniej oczywista, niż się wydaje.
Dwie ramki nachodzą na siebie tylko wtedy, gdy mają wspólny kawałek w obu osiach naraz. W poziomie i w pionie. Jeśli w poziomie pocisk jest dokładnie na środku kamienia, ale w pionie brakuje choćby jednego piksela, to się nie dotykają. Oko lubi patrzeć na jedną oś i uznać sprawę za załatwioną; ramki tak nie robią.
Teraz liczby. Pocisk to Rect(398, 300, 4, 18), kamień to Rect(380, 250, 48, 48). Policz, gdzie kończy się kamień w pionie i gdzie zaczyna się pocisk. Czy te dwie ramki na siebie nachodzą?
Nie nachodzą. Kamień zaczyna się na 250 i ma 48 pikseli wysokości, więc jego dół to 298, a ostatni wiersz jego pikseli to 297. Pocisk zaczyna się na 300. Między nimi są dwa puste piksele, i to wystarczy, żeby odpowiedź brzmiała „nie", choć w poziomie pocisk jest w samym środku kamienia. Za jedną klatkę pocisk poleci o dwanaście pikseli w górę, na 288, i wtedy wejdzie w kamień. Trafienie w grze to sprawa jednej klatki.
Tego rachunku nie musisz robić w pliku. Ramka umie odpowiedzieć sama, czy nachodzi na drugą ramkę. Ma do tego narzędzie, którego nazwa składa się z dwóch angielskich słów: „zderzać się" i skrótu od „prostokąt". Wołasz je na jednej ramce, podajesz drugą, a w odpowiedzi dostajesz prawdę albo fałsz. Nic więcej: żadnych liczb, żadnych współrzędnych, tylko True albo False. Taka odpowiedź pasuje prosto do if, i o to chodzi.
Uzupełnij tę samą nazwę w dwóch liniach. Pierwsza pyta o parę z poprzedniego kroku. Druga pyta o ten sam pocisk i kamień czterdzieści pikseli niżej. Program wypisze obie odpowiedzi.
Pierwsza para dała False, tak jak wyszło z rachunku. Druga dała True: kamień czterdzieści pikseli niżej zaczyna się na 290 i kończy na 338, więc pocisk od 300 jest w nim w pionie, a w poziomie był od początku. Dwa piksele różnicy w jedną stronę i odpowiedź się odwraca. Ramka liczy to za każdym razem dokładnie, bez zmęczenia i bez zaokrąglania na oko.
W grze nie ma jednego pocisku i jednego kamienia. Są dwie listy i każdy pocisk może trafić w każdy kamień, więc pytanie musi paść dla każdej pary. To znaczy: pętla po pociskach, w niej pętla po kamieniach, a w środku jedno pytanie do ramki. Poniżej masz gotowy kod z dwoma pociskami i dwoma kamieniami. Zanim go uruchomisz, policz, ile pytań padnie. Potem kliknij Uruchom pomiar i odczytaj z wierszy, która para trafiła.
Cztery pytania, bo każdy z dwóch pocisków został zapytany o każdy z dwóch kamieni. Trzy odpowiedzi to False, jedna to True, i po kolejności wierszy widać, że trafił drugi pocisk w drugi kamień. W prawdziwej grze przy dziesięciu pociskach i dwudziestu kamieniach to dwieście pytań na klatkę, dwanaście tysięcy na sekundę. Brzmi jak dużo, ale każde pytanie to porównanie czterech liczb, a komputer robi takich miliony na sekundę. To jest w porządku.
Pytanie parami to połowa roboty. Druga połowa to zapamiętanie, kto trafił i kto został trafiony, bo trafiony kamień ma zniknąć, a pocisk razem z nim. Funkcja, którą za chwilę napiszesz, prowadzi dwie listy pomocnicze: trafione na kamienie i zuzyte na pociski. Gdy para trafia, kamień idzie na pierwszą listę, pocisk na drugą.
Warunek przy każdej parze ma trzy części połączone przez and: ramki na siebie nachodzą, tego kamienia nie ma jeszcze w trafione i tego pocisku nie ma jeszcze w zuzyte. Wszystkie trzy muszą być prawdą naraz. Teraz przypadek do przemyślenia: jeden pocisk nachodzi naraz na dwa kamienie, które spadają blisko siebie. Pętla pyta o pierwszą parę, potem o drugą. Prześledź warunek dla obu par po kolei i odpowiedz: ile kamieni zniknie?
Jeden. Przy pierwszej parze wszystkie trzy części są prawdą, więc kamień idzie do trafione, a pocisk do zuzyte. Przy drugiej parze ramki wciąż nachodzą i drugiego kamienia nie ma w trafione, ale pocisk jest już w zuzyte, więc trzecia część jest fałszem i cały warunek też. Drugi kamień leci dalej. Jeden pocisk, jedno trafienie, tak jak w prawdziwej strzelance, i to właśnie te dwie listy tego pilnują.
sprawdz_trafieniaDostajesz funkcję sprawdz_trafienia(pociski, asteroidy) z gotowym początkiem i końcem. Na początku są dwie puste listy pomocnicze. Na końcu są dwa sita, które znasz z pocisków: jedno przepuszcza kamienie spoza trafione, drugie pociski spoza zuzyte. Brakuje serca: pętli po pociskach, w niej pętli po kamieniach, warunku z trzema częściami z poprzedniego kroku i dwóch append, które dopisują trafiony kamień i zużyty pocisk do list pomocniczych. Napisz to serce.
Zanim napiszesz, spójrz na ostatnią linię, bo jest w niej nowość. Dotąd funkcja oddawała jedną rzecz. Ta oddaje dwie listy naraz: return, po nim pierwsza lista, przecinek, druga lista. Po stronie main odbiera się to linią z przecinkiem po lewej: pociski, asteroidy = sprawdz_trafienia(pociski, asteroidy). Pierwsza oddana lista trafia do pierwszej nazwy, druga do drugiej, kolejność ma znaczenie. Sprawdzarka poda trzy pociski i cztery kamienie ułożone tak, że dwa pociski nachodzą na ten sam kamień, a jeden z nich dodatkowo na drugi; zobaczy, ile wróci z każdej listy.
Wróciły dwa pociski i trzy kamienie: jedno trafienie z dwunastu par, dokładnie tyle, ile pozwalają listy pomocnicze. Tak wygląda cała funkcja. Wpisz ją do pliku po rusz_asteroidy, a przed rysuj_tlo:
def sprawdz_trafienia(pociski, asteroidy):
trafione = []
zuzyte = []
for pocisk in pociski:
for asteroida in asteroidy:
if (pocisk.colliderect(asteroida["prostokat"])
and asteroida not in trafione
and pocisk not in zuzyte):
trafione.append(asteroida)
zuzyte.append(pocisk)
zostaly = [a for a in asteroidy if a not in trafione]
return [p for p in pociski if p not in zuzyte], zostaly
Nawiasy wokół warunku pozwalają rozbić go na trzy linie, po jednej na każdą część, żeby dało się go przeczytać. Plik rośnie jeszcze w jednym miejscu: w szufladzie LICZ, po obu ruchach, a przed tłem. Tam main odbiera dwie listy naraz:
pociski = rusz_pociski(pociski)
asteroidy = rusz_asteroidy(asteroidy)
pociski, asteroidy = sprawdz_trafienia(pociski, asteroidy)
rysuj_tlo(ekran, gwiazdy)
Zanim klikniesz, zapamiętaj plan: klikasz w okno gry, czekasz, aż kamień spadnie nad statek, i strzelasz w niego jeden raz. Kliknij ▶ Uruchom, kliknij w okno gry i trafij jeden kamień.
Kamień zniknął w chwili, gdy pocisk go dotknął, a pocisk zniknął razem z nim. To sprawdz_trafienia zapytała każdą parę, dopisała tę jedną do list pomocniczych, a dwa sita oddały listy bez nich. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie już przycisk Dalej.
import random
import pygame
SZEROKOSC = 800
WYSOKOSC = 600
KATALOG = "assety"
CZARNY = (26, 28, 64)
STATEK = [
".......11.......",
".......44.......",
"......1441......",
"......1331......",
".....143341.....",
".....143341.....",
"....11433411....",
"...1143333411...",
"..114333333411..",
".11433355334411.",
"1143333553333411",
"1433333553333341",
"1133311551333311",
"..11.1677761.11.",
"......167761....",
".......1771.....",
]
KOLORY = {
"1": (18, 22, 48),
"3": (150, 170, 210),
"4": (225, 240, 255),
"5": (0, 200, 255),
"6": (255, 150, 40),
"7": (255, 230, 120),
}
def zbuduj_statek(ksztalt, kolory, skala):
szerokosc = len(ksztalt[0])
wysokosc = len(ksztalt)
obraz = pygame.Surface((szerokosc, wysokosc), pygame.SRCALPHA)
for y in range(wysokosc):
for x in range(szerokosc):
znak = ksztalt[y][x]
if znak in kolory:
obraz.set_at((x, y), kolory[znak])
return pygame.transform.scale(obraz,
(szerokosc * skala, wysokosc * skala))
def wczytaj_obrazki():
obrazki = {"duza": [], "srednia": [], "mala": [], "wybuch": []}
for rozmiar in ("duza", "srednia", "mala"):
for numer in (1, 2, 3):
plik = f"{KATALOG}/asteroida_{rozmiar}_{numer}.png"
obrazki[rozmiar].append(pygame.image.load(plik))
for numer in (1, 2, 3, 4):
obrazki["wybuch"].append(
pygame.image.load(f"{KATALOG}/wybuch_{numer}.png"))
return obrazki
def losuj_gwiazdy(ile):
gwiazdy = []
for numer in range(ile):
x = random.randint(0, SZEROKOSC)
y = random.randint(0, WYSOKOSC)
jasnosc = random.choice([70, 110, 160, 210])
gwiazdy.append((x, y, jasnosc))
return gwiazdy
def nowy_statek(obraz):
prostokat = obraz.get_rect()
prostokat.centerx = SZEROKOSC // 2
prostokat.bottom = WYSOKOSC - 20
return {"obraz": obraz, "prostokat": prostokat, "predkosc": 6}
def losuj_asteroide(obrazki):
rozmiar = random.choice(["duza", "duza", "srednia", "mala"])
obraz = random.choice(obrazki[rozmiar])
prostokat = obraz.get_rect()
prostokat.x = random.randint(0, SZEROKOSC - prostokat.width)
prostokat.bottom = 0
return {"obraz": obraz, "prostokat": prostokat, "rozmiar": rozmiar,
"predkosc": random.randint(1, 3)}
def steruj_statkiem(statek, klawisze):
prostokat = statek["prostokat"]
if klawisze[pygame.K_LEFT]:
prostokat.x -= statek["predkosc"]
if klawisze[pygame.K_RIGHT]:
prostokat.x += statek["predkosc"]
if klawisze[pygame.K_UP]:
prostokat.y -= statek["predkosc"]
if klawisze[pygame.K_DOWN]:
prostokat.y += statek["predkosc"]
prostokat.clamp_ip(pygame.Rect(0, 0, SZEROKOSC, WYSOKOSC))
def wystrzel(pociski, statek):
pocisk = pygame.Rect(0, 0, 4, 18)
pocisk.centerx = statek["prostokat"].centerx
pocisk.bottom = statek["prostokat"].top
pociski.append(pocisk)
def rusz_pociski(pociski):
for pocisk in pociski:
pocisk.y -= 12
return [p for p in pociski if p.bottom > 0]
def rusz_asteroidy(asteroidy):
for asteroida in asteroidy:
asteroida["prostokat"].y += asteroida["predkosc"]
return [a for a in asteroidy if a["prostokat"].top < WYSOKOSC]
def sprawdz_trafienia(pociski, asteroidy):
trafione = []
zuzyte = []
for pocisk in pociski:
for asteroida in asteroidy:
if (pocisk.colliderect(asteroida["prostokat"])
and asteroida not in trafione
and pocisk not in zuzyte):
trafione.append(asteroida)
zuzyte.append(pocisk)
zostaly = [a for a in asteroidy if a not in trafione]
return [p for p in pociski if p not in zuzyte], zostaly
def rysuj_tlo(ekran, gwiazdy):
ekran.fill(CZARNY)
for x, y, jasnosc in gwiazdy:
pygame.draw.circle(ekran, (jasnosc, jasnosc, jasnosc + 20), (x, y), 1)
def main():
pygame.init()
ekran = pygame.display.set_mode((SZEROKOSC, WYSOKOSC))
zegar = pygame.time.Clock()
obrazki = wczytaj_obrazki()
obraz_statku = zbuduj_statek(STATEK, KOLORY, 3)
gwiazdy = losuj_gwiazdy(90)
statek = nowy_statek(obraz_statku)
pociski = []
asteroidy = []
licznik = 0
gramy = True
while gramy:
for zdarzenie in pygame.event.get():
if zdarzenie.type == pygame.QUIT:
gramy = False
if zdarzenie.type == pygame.KEYDOWN:
if zdarzenie.key == pygame.K_SPACE:
wystrzel(pociski, statek)
steruj_statkiem(statek, pygame.key.get_pressed())
licznik += 1
if licznik % 30 == 0:
asteroidy.append(losuj_asteroide(obrazki))
pociski = rusz_pociski(pociski)
asteroidy = rusz_asteroidy(asteroidy)
pociski, asteroidy = sprawdz_trafienia(pociski, asteroidy)
rysuj_tlo(ekran, gwiazdy)
for asteroida in asteroidy:
ekran.blit(asteroida["obraz"], asteroida["prostokat"])
for pocisk in pociski:
pygame.draw.rect(ekran, (255, 240, 200), pocisk)
ekran.blit(statek["obraz"], statek["prostokat"])
pygame.display.flip()
zegar.tick(60)
pygame.quit()
main()
Jeden obieg pętli ma teraz stałą kolejność w szufladzie LICZ i na początku MALUJ: ruch pocisków, ruch kamieni, sprawdzenie trafień, tło. Ułóż te cztery linie z klocków. Klocków jest pięć, jeden nie pasuje; odłóż go przyciskiem na bok.
Jedna rzecz o kolejności jest ważna. Sprawdzasz po ruchu, nigdy przed. Pytanie do ramki brzmi „czy nachodzicie na siebie teraz", więc musi paść wtedy, gdy ramki są już tam, gdzie będą narysowane. Sprawdzenie przed ruchem pytałoby o miejsca sprzed klatki i przepuszczałoby trafienia, które za chwilę będzie widać na ekranie.
Odłożony klocek wołał sprawdz_trafienia bez pociski, asteroidy = z przodu. To ta sama pomyłka, co przy pociskach: funkcja oddaje dwie nowe listy, a nikt ich nie odbiera, więc przepadają i gra dalej trzyma stare. Trafienia byłyby liczone w każdej klatce i nic by z nich nie wynikało. Python nie zgłosiłby błędu, kamienie po prostu dalej spadałyby przez pociski.
Czas zagrać. Plan: klikasz w okno gry, zestrzeliwujesz kilka kamieni i próbujesz trafić dwa naraz jednym pociskiem, gdy spadają blisko siebie. Patrz, co dzieje się z pociskiem po trafieniu i czy drugi kamień z pary też znika. Kliknij ▶ Uruchom, kliknij w okno gry i zestrzel kilka kamieni, w tym spróbuj dwóch naraz.
Trafiony kamień znika, pocisk znika razem z nim, a dwóch naraz jednym pociskiem trafić się nie da: drugi leci dalej, tak jak wyszło w czwartym kroku. To jest gra, w którą da się grać. Uczciwie: nie liczy jeszcze punktów, kamień wciąż przelatuje przez statek, a trafiony znika bez śladu. Każda z tych trzech rzeczy jest w dalszej części kursu. Gdy zwiniesz Kokpit na jego pasku, pod tym krokiem będzie przycisk Dalej.
wystrzel od zeraSiedem lekcji temu statek tylko płynął po niebie. Dziś strzela, kamienie spadają, a trafione znikają. Przez cały ten czas każda funkcja powstawała z pomocą: raz z klocków, raz z lukami, raz z gotowym listingiem do przepisania. Ten egzamin sprawdza, ile z tych funkcji masz w głowie, a nie w lekcji obok.
Jedna zasada na cztery kroki: nie zaglądasz do poprzednich lekcji. Nie dlatego, że to zabronione. Dlatego, że z otwartą lekcją obok odpowiedź napisze się sama, a Ty nie dowiesz się o sobie niczego. Zamknij je, zostaw ten jeden krok i zobacz, co zostało.
Na lekcji o spacji ciało funkcji wystrzel było najpierw układanką z klocków, a potem pisało się je ręką z opisem obok. Teraz jest tylko nagłówek def wystrzel(pociski, statek): i pusta linia pod nim. Napisz cztery linie, które rodzą pocisk na dziobie: prostokąt cztery na osiemnaście, w osi statku, tuż nad nim, dołożony do listy. Za które uchwyty chwycić, masz w głowie od czterech lekcji. Werdykt pod edytorem strzela raz i czyta listę.
rusz_pociski od zeraPocisk urodzony na dziobie sam nie poleci. Na lekcji o lecących pociskach powstała funkcja, która robi dwie rzeczy w stałej kolejności: najpierw przesuwa każdy pocisk o dwanaście pikseli w górę, potem przesiewa listę i oddaje nową, tylko z tymi, które są jeszcze w oknie. Kolejność ma znaczenie i wiesz dlaczego.
Znowu jest tylko nagłówek def rusz_pociski(pociski):. Napisz ruch i sito, z return na końcu. Werdykt poda trzy pociski w różnych miejscach i sprawdzi, które wrócą.
Ten krok nie ma nic do uruchomienia i to jest jego zaleta. Komputer przeszedłby tę pętlę w ułamku sekundy i pokazał cztery odpowiedzi, a nie byłoby wiadomo, czy rozumiesz, czy tylko patrzysz. Tym razem pętlę wykonujesz Ty, przy stole, tak jak w Podstawach śledziło się kod palcem.
W treści ćwiczenia są dwa pociski i dwa kamienie, podane liczbami. Funkcja od trafień pyta każdy pocisk o każdy kamień: pętla po pociskach, w niej pętla po kamieniach. Przejdź te pary w tej kolejności i dla każdej wpisz, co odpowiedziałaby ramka: True albo False. Pamiętaj, że ramki nachodzą na siebie tylko wtedy, gdy mają wspólny kawałek w obu osiach naraz. Gdzie kończy się kamień i gdzie zaczyna pocisk, liczysz z czterech liczb każdej ramki, sam.
Ostatni krok to cały obieg pętli, wszystko, co dzieje się po skrzynce ze zdarzeniami, od sterowania statkiem do zegara. Piętnaście linii, pomieszanych i bez wcięć. Ułóż je w kolejności, w jakiej wykonuje je jeden obieg, i wsuń każdą tak głęboko, jak jest wcięta w Twoim pliku. Trzy szuflady i ich porządek masz z lekcji o metronomie; tu nikt Ci ich nie przypomni i o to chodzi.
W tym kroku nie ma nic do klikania. Werdykt uruchamia grę z Twoim obiegiem sam i patrzy, czy statek płynie, pociski lecą, kamienie spadają i trafienia działają.
Masz w głowie grę, w którą da się grać: statek z granicami, spację, która rodzi pocisk, pociski, które lecą i znikają, kamienie z plików spadające co pół sekundy i trafienia, które je zdejmują. Wszystko, co dojdzie do pliku od następnej lekcji, stanie na tym: punkty, wybuchy, zderzenia ze statkiem, dźwięk. Żadna z tych rzeczy nie jest nowym szkieletem. Każda to jedna nowa funkcja i jedna nowa linia w obiegu, który właśnie ułożyło się z pamięci.