Kategória: Python gyerekeknek.
Áttekintés
A feladat majdnem kész, de csak majdnem. Sok apróság van még hátra. Létezik egy úgynevezett Pareto-elv (ez egy általános elv), ami jelen esetben azt jelenti, hogy egy szoftver 80%-os készültségére megy az idő 20%-a, és fordítva. Most tartunk a 80% készültségnél és 20% időnél. Tehát akár még négyszer ennyi időt is el lehetne tölteni a csiszolásával, amelynél az eredmény alig látható, és sok időbe telik a megvalósítása. Most pár ilyet nézünk meg!
Kisebb hibák javítása
Golyó a végén
Ha befejeződött a játszma, és lövünk egyet, akkor megjelenik egy golyó. Megoldás: a key_pressed() függvény elejére (a global után) szúrjuk be az alábbi két sort:
if lives <= 0: return
Beragadt űrhajó
Ha véletlenül egyszerre megnyomjuk a jobb és a bal nyilat, akkor nem halad tovább az űrhajó mindaddig, amíg el nem engedjük, és újra le nem nyomjuk. Ennek auz oka a következő:
- Lenyomjuk és nyomva tartjuk a bal nyilat. Az űrhajó left állapotba kerül.
- Lenyomjuk és nyomva tartjuk a jobb nyilat. Az űrhajó right állapotba kerül.
- Elengedjük a bal nyilat. Az űrhajó stand állapotba kerül.
- Nyomva tartjuk a jobb nyilat. Mivel nincs változás, az űrhajó stand állapotban marad. Újra el kell engedni és meg kell nyomni ahhoz, hogy haladjon.
A megoldás egy kicsit bonyolultabb. Ki kell törölni a rocketship_state változót. Ehelyett a következőket szúrjuk be. Hozzunk létre két globális változót, ami azt jelzi, hogy a bal ill. a jobb nyíl le van-e nyomva:
left_pressed = False right_pressed = False
A lenyomás vizsgálatakor (key_pressed) egyrészt jelezzük, hogy ezeket a változókat meg szeretnénk változtatni, másrészt a megfelelő gomb lenyomásakor állítsuk is be (a felesleges sorokat töröljük ki):
def key_pressed(event): global left_pressed, right_pressed […] if event.keysym == 'Left': left_pressed = True if event.keysym == 'Right': right_pressed = True
Az elengedéskor (key_released) is állítsuk be megfelelően:
def key_released(event): global left_pressed, right_pressed if event.keysym == 'Left': left_pressed = False # new 1.2 if event.keysym == 'Right': right_pressed = False # new 1.2
A fő függvényben pedig (play) ne a rocketship_state-et vizsgáljuk, hanem a lenyomás állapotát:
if left_pressed and rocketship_coords[0] > 0: # mod 1.2 canvas.move(rocketship, -1, 0) if right_pressed and rocketship_coords[0] < 480: # mod 1.2 canvas.move(rocketship, 1, 0)
Egész sok mindent meg kellett változtatnunk egyetlen apró hiba kijavítása érdekében.
Újraindítás
Látszólag egyszerű, ám nagyon sok helyen bele kell nyúlni a kódba, és sok a buktató.
Először is hozzuk létre a nyomógombot, közvetlenül az információs rész alatt:
restart_button = Button(master=root, text='Újraindít', command=init, state='disabled') restart_button.pack()
Fontos, hogy alapból inaktív legyen (state='disabled'), különben a fő ciklust is le kellene állítani.
Tehát az init függvény fog meghívódni, ha rákattintunk. Ekkor alapértelmezésre kell állítani a változókat. Ezt érdemes úgy megírni, hogy eleve az induláskor is meghívjuk, ám vannak részek, amelyek csak a második indítástól érdekesek:
- a szellemek törlése,
- a golyók törlése,
- az űrhajó alaphelyzetbe állítása.
Ennek tehát lesz egy paramétere, amellyel megmondjuk, hogy ez az első futtatás-e, vagy nem. Alapértelmezett értéket is fogunk neki adni.
Töröljük ki az elejéről az alábbi sorokat:
lives = 3 score = 0 counter = 0 left_pressed = False right_pressed = False ghosts = [] bullets = []
Az init() függvény a következő:
def init(first_run=False): global lives, score, counter, left_pressed, right_pressed, ghosts, bullets if not first_run: for ghost_struct in ghosts: canvas.delete(ghost_struct['ghost']) for bullet in bullets: canvas.delete(bullet) rocketship_coords = canvas.coords(rocketship) canvas.move(rocketship, 240 - rocketship_coords[0], 310 - rocketship_coords[1]) lives = 3 score = 0 counter = 0 left_pressed = False right_pressed = False ghosts = [] bullets = [] lives_nr_label['text'] = str(lives) score_nr_label['text'] = str(score) restart_button['state'] = 'disabled' canvas.itemconfigure(end_text, state='hidden') root.after(2, play)
Magyarázat:
- Először megadjuk a globális változókat, amelyeket itt hozunk létre.
- Ha nem az első futásról van szó, akkor töröljük a szellemeket és a golyókat, valamint alaphelyzetbe állítjuk az űrhajót.
- Beállítjuk a korábbi lépésben törölt adatokat: az életek számát, a pontszámot, a számlálót, a gomb lenyomások állapotát, a szellemek és a golyók listáját.
- Át kell kicsit rendezni a főprogramot (ld. lejjebb); emiatt csak itt tudjuk megjeleníteni az életek számát és a pontszámot.
- Ugyancsak az átszervezés miatt nem a végén hozzuk létre a VÉGE feliratot, hanem már itt, láthatatlanná tesszük (state='hidden'), és szükség esetén ismét láthatóvá.
- Innen indítjuk a fő ciklust.
A fő ciklusban ha az életek száma elfogyott, akkor aktiváljuk az új játék nyomógombot, és nem létrehozzuk a VÉGE feliratot, hanem megjelenítjük (itt csak a második és harmadik sor új ill. módosult):
if lives <= 0: restart_button['state'] = 'active' canvas.itemconfigure(end_text, state='normal') else: root.after(2, play)
A végén a root.after(2, play) sort ki kell törölni, az átkerült a fenti függvénybe.
Mivel még nem áll rendelkezésre, nem írjuk ki az életek számát és a pontszámot sem. A módosított kódrészlet:
lives_str_label = Label(master=info_frame, text='Életek száma:') lives_str_label.grid(column=0, row=0) lives_nr_label = Label(master=info_frame) # <- módosult lives_nr_label.grid(column=1, row=0) score_str_label = Label(master=info_frame, text='Pontszám:') score_str_label.grid(column=0, row=1) score_nr_label = Label(master=info_frame) # <- módosult score_nr_label.grid(column=1, row=1)
Miután létrehoztuk a vásznat, adjun hozzá a láthatatlan (state='hidden') VÉGE feliratot:
end_text = canvas.create_text(240, 180, text='VÉGE', fill='yellow', font=('Helvetica', 50), state='hidden')
Közvetlenül a root.mainloop() előtt hívjuk meg az inicializálást:
init(True)
Mivel az életek száma és a pontszám csak ekkor jön létre, korábban nem tudtuk beállítani. Ám az inicializálás meghívását viszont csak a vászon létrehozása utánra tehetjük. Szóval nem egyszerű! És ezzel nem kezeltük azt a problémát, hogy ha futás közben kattintana a játékos az újraindításra, akkor helytelenül működne (emiatt állítottuk inaktívra). Egyetlen aprónak tűnő módosítás igen sok aprólékos változtatást tett szükségessé.
Refaktorálás
A kód lényegében készen van. Ám eléggé nehézkessé vált a karbantartása: pl. egyetlen nyomógomb létrehozásához is nagyon sok helyen bele kellett nyúlni. A fő ciklus különösen hosszú lett, nehezen áttekinthető. Eljött az idő a kód olyan szintű átalakítására, ami új funkciót nem visz bele, az olvashatóságit viszont javítja. A következőket tesszük bele:
- A fő programot (ami most "csak úgy" oda van írva) külön függvénybe tesszük. Az igazi főprogram tehát csak annak a függvénynek a meghívása lesz. Viszont itt globálissá kell tennünk azokat a változókat, amelyeket a többi függvényből is használni szeretnénk (akár csak olvasni).
- A play() függvényt logikai egységekre bontjuk: üres sortokat és megjegyzéseket írunk a logikai egységek közé.
A végeredmény:
from tkinter import * import random def key_pressed(event): global left_pressed, right_pressed if lives <= 0: return if event.keysym == 'Left': left_pressed = True if event.keysym == 'Right': right_pressed = True if event.char == ' ': rocketship_coords = canvas.coords(rocketship) position_x = rocketship_coords[0] - 10 position_y = rocketship_coords[1] - 50 bullet = canvas.create_oval( position_x, position_y, position_x + 20, position_y + 20, width=3, outline='black', fill='yellow' ) bullets.append(bullet) def key_released(event): global left_pressed, right_pressed if event.keysym == 'Left': left_pressed = False if event.keysym == 'Right': right_pressed = False def play(): global counter, lives, score # move rocketship rocketship_coords = canvas.coords(rocketship) if left_pressed and rocketship_coords[0] > 0: canvas.move(rocketship, -1, 0) if right_pressed and rocketship_coords[0] < 480: canvas.move(rocketship, 1, 0) # move bullets; delete if reaches the edge deleted_bullets = [] for bullet in bullets: if canvas.coords(bullet)[1] > 0: canvas.move(bullet, 0, -1) else: canvas.delete(bullet) deleted_bullets.append(bullet) for deleted_bullet in deleted_bullets: bullets.remove(deleted_bullet) # move ghosts ghosts_to_delete = [] bullets_to_delete = [] for ghost_struct in ghosts: ghost = ghost_struct['ghost'] ghost_coords = canvas.coords(ghost) for bullet in bullets: bullet_coords = canvas.coords(bullet) coord_diff_x = abs(bullet_coords[0] - ghost_coords[0]) coord_diff_y = abs(bullet_coords[1] - ghost_coords[1]) if coord_diff_x < 20 and coord_diff_y < 20: ghosts_to_delete.append(ghost_struct) bullets_to_delete.append(bullet) score = score + 1 score_nr_label['text'] = str(score) for ghost_to_delete in ghosts_to_delete: canvas.delete(ghost_to_delete['ghost']) ghosts.remove(ghost_to_delete) for bullet_to_delete in bullets_to_delete: canvas.delete(bullet_to_delete) bullets.remove(bullet_to_delete) # create ghosts counter = counter + 1 if counter % 100 == 0: ghost = canvas.create_image(random.randint(0, 480), 0, image=ghost_image) direction = random.randint(-2, 2) ghost_struct = { 'ghost': ghost, 'direction': direction, } ghosts.append(ghost_struct) # check bullet hit deleted_ghosts = [] hit = False for ghost_struct in ghosts: ghost = ghost_struct['ghost'] direction = ghost_struct['direction'] ghost_coords = canvas.coords(ghost) if ghost_coords[1] < 360: if direction == 0: canvas.move(ghost, 0, 1) elif abs(direction) == 1: canvas.move(ghost, direction/3, 0.95) else: canvas.move(ghost, direction/3, 0.75) else: canvas.delete(ghost) deleted_ghosts.append(ghost_struct) coord_diff_x = abs(ghost_coords[0] - rocketship_coords[0]) coord_diff_y = abs(ghost_coords[1] - rocketship_coords[1]) if coord_diff_x <= 40 and coord_diff_y <= 40: hit = True lives = lives - 1 lives_nr_label['text'] = str(lives) for deleted_ghost in deleted_ghosts: ghosts.remove(deleted_ghost) if hit: for ghost_struct in ghosts: canvas.delete(ghost_struct['ghost']) ghosts.clear() for bullet in bullets: canvas.delete(bullet) bullets.clear() # end check if lives <= 0: restart_button['state'] = 'active' canvas.itemconfigure(end_text, state='normal') else: root.after(2, play) def init(first_run=False): global lives, score, counter, left_pressed, right_pressed, ghosts, bullets if not first_run: for ghost_struct in ghosts: canvas.delete(ghost_struct['ghost']) for bullet in bullets: canvas.delete(bullet) rocketship_coords = canvas.coords(rocketship) canvas.move(rocketship, 240 - rocketship_coords[0], 310 - rocketship_coords[1]) lives = 3 score = 0 counter = 0 left_pressed = False right_pressed = False ghosts = [] bullets = [] lives_nr_label['text'] = str(lives) score_nr_label['text'] = str(score) restart_button['state'] = 'disabled' canvas.itemconfigure(end_text, state='hidden') root.after(2, play) def start(): global root, canvas, lives_nr_label, score_nr_label, rocketship, ghost_image, end_text, restart_button root = Tk() root.title('Űrhajós játék') root.bind('<KeyPress>', key_pressed) root.bind('<KeyRelease>', key_released) info_frame = Frame(root) lives_str_label = Label(master=info_frame, text='Életek száma:') lives_str_label.grid(column=0, row=0) lives_nr_label = Label(master=info_frame) lives_nr_label.grid(column=1, row=0) score_str_label = Label(master=info_frame, text='Pontszám:') score_str_label.grid(column=0, row=1) score_nr_label = Label(master=info_frame) score_nr_label.grid(column=1, row=1) info_frame.pack() restart_button = Button(master=root, text='Újraindít', command=init, state='disabled') restart_button.pack() canvas = Canvas(root, width=480, height=360, bg='black') canvas.pack() rocketship_image = PhotoImage(file='rocketship.png') rocketship = canvas.create_image(240, 310, image=rocketship_image) ghost_image = PhotoImage(file='ghost.png') end_text = canvas.create_text(240, 180, text='VÉGE', fill='yellow', font=('Helvetica', 50), state='hidden') init(True) root.mainloop() start()
Magyarítás
Talán nehéz követni azt, hogy mi az, amit mindenképp úgy kell írni, és mi az, ahol szabadságunk van valamit máshogy elnevezni. Emiatt elkészítettem a program magyarított változatát: lényegében ami angolul van, azt úgy kell írni, de ami magyarul, ott szabadon dönthetünk.
A képek magyar névvel innen letölthetőek:
from tkinter import * import random def gomb_lenyomva(lenyomva_esemény): global bal_lenyomva, jobb_lenyomva if életek_száma <= 0: return if lenyomva_esemény.keysym == 'Left': bal_lenyomva = True if lenyomva_esemény.keysym == 'Right': jobb_lenyomva = True if lenyomva_esemény.char == ' ': űrhajó_koordináták = vászon.coords(rocketship) x_koordináta = űrhajó_koordináták[0] - 10 y_koordináta = űrhajó_koordináták[1] - 50 golyó = vászon.create_oval( x_koordináta, y_koordináta, x_koordináta + 20, y_koordináta + 20, width=3, outline='black', fill='yellow' ) golyók.append(golyó) def gomb_felengedve(felengedve_esemény): global bal_lenyomva, jobb_lenyomva if felengedve_esemény.keysym == 'Left': bal_lenyomva = False if felengedve_esemény.keysym == 'Right': jobb_lenyomva = False def játszik(): global számláló, életek_száma, score # űrhajó mozgatása rocketship_coords = vászon.coords(rocketship) if bal_lenyomva and rocketship_coords[0] > 0: vászon.move(rocketship, -1, 0) if jobb_lenyomva and rocketship_coords[0] < 480: vászon.move(rocketship, 1, 0) # golyók mozgatása; törölni kell, ha eléri a szélét deleted_bullets = [] for bullet in golyók: if vászon.coords(bullet)[1] > 0: vászon.move(bullet, 0, -1) else: vászon.delete(bullet) deleted_bullets.append(bullet) for deleted_bullet in deleted_bullets: golyók.remove(deleted_bullet) # szellemek mozgatása ghosts_to_delete = [] bullets_to_delete = [] for ghost_struct in ghosts: ghost = ghost_struct['ghost'] ghost_coords = vászon.coords(ghost) for bullet in golyók: bullet_coords = vászon.coords(bullet) coord_diff_x = abs(bullet_coords[0] - ghost_coords[0]) coord_diff_y = abs(bullet_coords[1] - ghost_coords[1]) if coord_diff_x < 20 and coord_diff_y < 20: ghosts_to_delete.append(ghost_struct) bullets_to_delete.append(bullet) score = score + 1 score_nr_label['text'] = str(score) for ghost_to_delete in ghosts_to_delete: vászon.delete(ghost_to_delete['ghost']) ghosts.remove(ghost_to_delete) for bullet_to_delete in bullets_to_delete: vászon.delete(bullet_to_delete) golyók.remove(bullet_to_delete) # szellemek létrehozása számláló = számláló + 1 if számláló % 100 == 0: ghost = vászon.create_image(random.randint(0, 480), 0, image=ghost_image) direction = random.randint(-2, 2) ghost_struct = { 'ghost': ghost, 'direction': direction, } ghosts.append(ghost_struct) # golyó találat ellenőrzés deleted_ghosts = [] hit = False for ghost_struct in ghosts: ghost = ghost_struct['ghost'] direction = ghost_struct['direction'] ghost_coords = vászon.coords(ghost) if ghost_coords[1] < 360: if direction == 0: vászon.move(ghost, 0, 1) elif abs(direction) == 1: vászon.move(ghost, direction/3, 0.95) else: vászon.move(ghost, direction/3, 0.75) else: vászon.delete(ghost) deleted_ghosts.append(ghost_struct) coord_diff_x = abs(ghost_coords[0] - rocketship_coords[0]) coord_diff_y = abs(ghost_coords[1] - rocketship_coords[1]) if coord_diff_x <= 40 and coord_diff_y <= 40: hit = True életek_száma = életek_száma - 1 lives_nr_label['text'] = str(életek_száma) for deleted_ghost in deleted_ghosts: ghosts.remove(deleted_ghost) if hit: for ghost_struct in ghosts: vászon.delete(ghost_struct['ghost']) ghosts.clear() for bullet in golyók: vászon.delete(bullet) golyók.clear() # vége ellenőrzés if életek_száma <= 0: restart_button['state'] = 'active' vászon.itemconfigure(end_text, state='normal') else: főablak.after(2, játszik) def kezdeti_értékeket_beállít(first_run=False): global életek_száma, score, számláló, bal_lenyomva, jobb_lenyomva, ghosts, golyók if not first_run: for ghost_struct in ghosts: vászon.delete(ghost_struct['ghost']) for bullet in golyók: vászon.delete(bullet) rocketship_coords = vászon.coords(rocketship) vászon.move(rocketship, 240 - rocketship_coords[0], 310 - rocketship_coords[1]) életek_száma = 3 score = 0 számláló = 0 bal_lenyomva = False jobb_lenyomva = False ghosts = [] golyók = [] lives_nr_label['text'] = str(életek_száma) score_nr_label['text'] = str(score) restart_button['state'] = 'disabled' vászon.itemconfigure(end_text, state='hidden') főablak.after(2, játszik) def indít(): global főablak, vászon, lives_nr_label, score_nr_label, rocketship, ghost_image, end_text, restart_button főablak = Tk() főablak.title('Űrhajós játék') főablak.bind('<KeyPress>', gomb_lenyomva) főablak.bind('<KeyRelease>', gomb_felengedve) info_frame = Frame(főablak) lives_str_label = Label(master=info_frame, text='Életek száma:') lives_str_label.grid(column=0, row=0) lives_nr_label = Label(master=info_frame) lives_nr_label.grid(column=1, row=0) score_str_label = Label(master=info_frame, text='Pontszám:') score_str_label.grid(column=0, row=1) score_nr_label = Label(master=info_frame) score_nr_label.grid(column=1, row=1) info_frame.pack() restart_button = Button(főablak, text='Újraindít', command=kezdeti_értékeket_beállít, state='disabled') restart_button.pack() vászon = Canvas(főablak, width=480, height=360, bg='black') vászon.pack() rocketship_image = PhotoImage(file='urhajo.png') rocketship = vászon.create_image(240, 310, image=rocketship_image) ghost_image = PhotoImage(file='szellem.png') end_text = vászon.create_text(240, 180, text='VÉGE', fill='yellow', font=('Helvetica', 50), state='hidden') kezdeti_értékeket_beállít(True) főablak.mainloop() indít()
Utolsó gondolatok
A Scratch után kifejezetten nehéz volt Pythonban is elkészíteni ugyanazt a játékot. Sokkal hosszabb és bonyolultabb lett a kód. Ám míg a Scratch-ben ezzel megközelítettük a lehetőségek határát, addig Pythonban épp, hogy csak bekukkantottunk a lehetőségek óriási tárházába.