Python gyerekeknek - űrhajós játék - utolsó simítások

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 meghvíjuk, á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') # new 3
    init(True)
    root.mainloop()
 
start()
urhajo_ujraindit.png

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.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License