Det har blivit dags för andra delen i min tutorial om hur man kan skapa ett litet 2D-spel i programspråket Python med hjälp av ramverket Pygame Zero. Den första delen hittar du här och ett introducerande blogginlägg om Pygame Zero ligger här. Handledningen vänder sig till nybörjare och du behöver inte mycket förkunskaper alls för att kunna hänga med (hoppas jag, om något är oklart får du gärna lämna en kommentar).

Sedan tidigare så har vi ett spelfönster och en kanon som vi kan styra och skjuta med. I den följande ”lektionen” så ska vi skapa de rymdvarelser som ska stå för motståndet i spelet. Vi ska även lägga till en poängräknare och lite bling-bling i form av ljud av kanonskott och explosioner. Slutresultatet kommer att se ut på det här viset:

space_invaders

Om du har gått igenom den första delen av denna tutorial så vet du säkert vad vi behöver göra när vi nu ska lägga till en ny typ av objekt: Vi behöver skapa ytterligare en klass. Lägg till följande kod i ”klassdelen” av programmet, under de befintliga klasserna Cannon och Bullet:

class Alien(Actor):
    def __init__(self, sprite, position):
        super(Alien, self).__init__(sprite, position)

För att kunna rita ut rymdvarelserna i spelfönstret så behöver vi också en bild. Högerklicka på bilden nedan och spara den som ”alien.png” i samma katalog som de övriga bilderna, ”images”.

alien
Vidare så behöver vi en ny lista, aliens, att förvara våra rymdvarelser i. Och så måste vi ju skapa var och en av rymdvarelserna. De är, som du ser här ovan, rätt många från början (35 stycken, närmare bestämt) så att tillverka dem ”för hand” skulle vara rätt trist. Det skulle resultera i 35 rader kod i stil med dessa:

aliens.append(Alien('alien', (60, 40)))
aliens.append(Alien('alien', (120, 40)))
aliens.append(Alien('alien', (180, 40)))
...

Lyckligtvis så finns det en konstruktion i Python (och typ alla andra programspråk) som passar perfekt i situationer som denna, och vi har redan använt den några gånger: For-loopen! Uppdatera koden i början av ”speldelen” på det här viset:

# Här startar själva spelet

cannon = Cannon('cannon', (WIDTH / 2, 560))
bullets = []
aliens = []

alien_x = 60
alien_y = 40
for i in range(7):
    aliens.append(Alien('alien', (alien_x, alien_y)))
    alien_x += 60

Ta sedan och lägg till ytterligare en for-loop, som går igenom vår lista med rymdvarelser och ritar var och en på skärmen, i metoden draw:

def draw():
    screen.clear()
    cannon.draw()
  
    for bullet in bullets:
        bullet.draw()
    
    for alien in aliens:
        alien.draw()

I den övre loopen ser vi, genom att skriva for i in range(7), till så att koden i det följande blocket körs just sju gånger. För varje varv skapas en ny rymdvarelse som läggs till i listan aliens. Dessutom så ökas x-positionen, så att nästa rymdvarelse hamnar 60 pixlar längre till höger. Om du nu sparar och provkör programmet så ser du förhoppningsvis en prydlig rad med rymdvarelser längst upp i spelfönstret.

Men det finns flera problem. För det första så vill vi ju ha betydligt fler rymdvarelser. För det andra så rör de sig inte ur fläcken. Och för det tredje så händer ingenting alls när vi skjuter på dem.

Det första problemet är faktiskt väldigt enkelt att lösa. Vi vill alltså ha fem rader med rymdvarelser i stället för en. Vad gör vi då? Jo, vi lägger den for-loop som skapar den första raden inuti en annan loop! I den yttre, omslutande loopen så plussar vi även på y-positionen för rymdvarelserna, så att varje ny rad hamnar 40 pixlar under den föregående. Och så sätter vi tillbaka x-positionen till 60 efter varje varv, så att raderna hela tiden börjar ritas på samma ställe i sidled. Bygg ut koden på det här viset:

alien_x = 60
alien_y = 40
for i in range(5):
    for i in range(7):
        aliens.append(Alien('alien', (alien_x, alien_y)))
        alien_x += 60
    alien_x = 60
    alien_y += 40

Så till det här med att få rymdvarelserna att röra sig. Precis som i den klassiska förlagan så vill vi både att de gradvis ska närma sig kanonen, det vill säga röra sig nedåt, och att de hela tiden ska förflytta sig fram och tillbaka i sidled så att de blir svårare att träffa. För att det här ska funka så behöver vi flera nya variabler. Dels behöver vi separata variabler för hastigheten i sidled och hastigheten i höjdled. Dels så behöver vi variabler för att hålla koll på hur mycket rymdvarelserna har rört sig i sidled och när det är dags för dem att vända och röra sig åt andra hållet.

Först gör vi följande tillägg i klassen Alien:

class Alien(Actor):
    def __init__(self, sprite, position):
        super(Alien, self).__init__(sprite, position)
        self.movement = 20
        self.max_movement = 40
        self.x_speed = 1
        self.y_speed = 7
    
    def update(self):
        self.x += self.x_speed
        self.movement += self.x_speed
        if abs(self.movement) >= self.max_movement:
            self.x_speed *= -1
            self.y += self.y_speed
            self.movement = 0

I __init__-metoden, eller konstruktorn som man också kan kalla den, sätter vi nu x-hastigheten till en pixel och y-hastigheten till sju pixlar. Vi sätter även gränsen för rörelser i sidled till 40 pixlar. Eftersom vi vill att rymdvarelserna ska röra sig både åt höger och åt vänster jämfört med utgångsläget så ”låtsas” vi att de, när spelet börjar, redan har rört sig 20 pixlar åt höger, dvs att de står i mitten.

I Aliens metod update så ökar vi både x-positionen och den variabel som mäter den sammanlagda förflyttningen, self.movement, med x-hastigheten (1). I if-satsen så kontrollerar vi sedan om rymdvarelsen har nått gränsen för förflyttningar, 40 pixlar. När rymdvarelsen rör sig åt vänster så kommer self.movement att innehålla ett negativt värde. Därför jämför vi i stället det så kallade absolutvärdet med maxvärdet genom att skriva if abs(self.movement) >= self.max_movement. Om absolutvärdet är 40 eller mer så inverterar vi x-hastigheten, om den är 1 så blir den -1 och är den -1 så blir den 1. På så vis får vi rymdvarelsen att byta riktning i sidled. Och så låter vi den hoppa ner ett snäpp i höjdled genom att öka y-positionen.

Vi behöver också lägga till ytterligare en liten for-loop längst ner i speldelens update-metod, under den loop som uppdaterar skotten från kanonen och tar bort dem om de hamnat utanför fönstret:

def update():
    if keyboard.right:
        cannon.move_right()
    elif keyboard.left:
        cannon.move_left()
  
    if keyboard.space:
        if get_ticks() - cannon.last_fire > cannon.firing_interval:
            bullets.append(Bullet('bullet', cannon.pos))
            cannon.last_fire = get_ticks()
      
    for bullet in bullets[:]:
        bullet.update()
        if bullet.is_dead():
            bullets.remove(bullet)
    
    for alien in aliens:
        alien.update()

Du kanske har noterat att den for-loop som går igenom listan bullets ser lite annorlunda ut jämfört med den som går igenom aliens. Jag ska strax berätta varför.

Som jag var inne på ovan så vill vi förstås också att något ska hända när vi skjuter på rymdvarelserna – de ska ”dö”, eller åtminstone skadas. För att åstadkomma detta så behöver vi hela tiden kontrollera om någon av rymdvarelserna har kolliderat med något av de skott som har avfyrats från kanonen. Det här är enklare än vad man kan tro. Du kanske minns från förra lektionen att våra spelobjekt ärver från klassen Actor, som är en färdig klass som ingår i Pygame Zero, och att de därmed får med sig en hel del egenskaper därifrån.

Bland annat så omges varje objekt i spelet av en rektangel som är osynlig men som gör det lätt att ta reda på var i fönstret som objektet befinner sig och var dess gränser går. Dessa rektanglar har en x- och en y-koordinat, en bredd och en höjd som motsvarar de sprites som används. Dessutom så har våra objekt en nedärvd metod, colliderect, som kan användas för att kontrollera om dess rektangel överlappar ett annat objekts rektangel. Den ska vi dra nytta av nu.

I speldelens update-metod, ta och uppdatera den avslutande for-loopen som går igenom listan med rymdvarelser på följande vis:

    for alien in aliens[:]:
        alien.update()
        for bullet in bullets[:]:
            if alien.colliderect(bullet):
                aliens.remove(alien)
                bullets.remove(bullet)

Som du ser så har jag lagt till tecknen [:] i for-satserna, både i den yttre och den inre loopen. Samma tecken, Pythons så kallade slice-operator, finns med i den for-loop som går igenom listan med skott från kanonen och uppdaterar dem, och det är av följande orsak: Att ändra på en lista, t ex genom att ta bort ett element, samtidigt som man loopar igenom den kan vara vanskligt – det kan ge oväntade resultat. Genom att lägga till slice-operatorn efter namnet på en lista så ser vi till så att det i stället är en kopia av listan som vi går igenom. Däremot så anropar vi, när vi vill ta bort ”döda” objekt, metoden remove på originallistorna.

Nu går det att skjuta rymdvarelserna. Men det är, åtminstone enligt min mening, alldeles för lätt att ha ihjäl dem – man behöver bara träffa med ett enda skott. Låt oss ge varje rymdvarelse tre liv, minska antalet liv när de träffas och ta bort dem först när antalet liv är noll. Först gör vi två tillägg i klassen Alien, variabeln self.lives och metoden is_dead:

class Alien(Actor):
    def __init__(self, sprite, position):
        super(Alien, self).__init__(sprite, position)
        self.movement = 20
        self.max_movement = 40
        self.x_speed = 1
        self.y_speed = 7
        self.lives = 3
    
    def update(self):
        self.x += self.x_speed
        self.movement += self.x_speed
        if abs(self.movement) >= self.max_movement:
            self.x_speed *= -1
            self.y += self.y_speed
            self.movement = 0
            
    def is_dead(self):
        return self.lives == 0

Och så ändrar vi koden lite grann i den for-loop i speldelens update-metod där vi kontrollerar om någon rymdvarelse har kolliderat med ett skott:

    for alien in aliens[:]:
        alien.update()
        for bullet in bullets[:]:
            if alien.colliderect(bullet):
                alien.lives -= 1
                if alien.is_dead():
                    aliens.remove(alien)
                bullets.remove(bullet)

Mer än så krävs inte för att rymdvarelserna ska bli mer seglivade. Prova, så får du se.

Om du nu börjar krokna så har jag all förståelse. Men härda ut – nu återstår bara det där bling-blinget och det är inte särskilt kodkrävande. Börja med att ladda ner ljudfilerna ”explosion.wav” och ”shot.wav” som ligger här respektive här. Självklart kan du, om du föredrar det, skapa ljudeffekterna själv med ett verktyg i stil med Bfxr men ge dem i så fall samma namn. Skapa en katalog som heter ”sounds” i den katalog där ditt program ligger och lägg ljudfilerna i den. Frivillig överkurs är att även ladda ner typsnittet Space Invaders, exempelvis från denna sajt, och lägga filen (”space_invaders.ttf”) i en katalog som heter ”fonts”.

Sedan ska vi se till så att kanonen, det vill säga spelaren, kan få poäng och att dessa skrivs ut på skärmen. Lägg till variabeln self.score i klassen init-metoden i klassen Cannon och sätt den till noll.

class Cannon(Actor):
    def __init__(self, sprite, position):
        super(Cannon, self).__init__(sprite, position)
        self.speed = 5
        self.last_fire = 0
        self.firing_interval = 300
        self.score = 0

Och så behöver vi göra ett par tillägg i speldelens update-metod. Först i den if-sats som kontrollerar om space-tangenten är nedtryckt, där vi ska se till att skott-ljudet spelas upp…

    if keyboard.space:
        if get_ticks() - cannon.last_fire > cannon.firing_interval:
            bullets.append(Bullet('bullet', cannon.pos))
            sounds.shot.play()
            cannon.last_fire = get_ticks()

… och sedan i for-loopen där vi kontrollerar om några kollisioner har inträffat mellan rymdvarelser och skott. Där ska vi både spela upp explosions-ljudet och öka antalet poäng med 100 när en rymdvarelse dör.

    for alien in aliens[:]:
        alien.update()
        for bullet in bullets[:]:
            if alien.colliderect(bullet):
                alien.lives -= 1
                if alien.is_dead():
                    aliens.remove(alien)
                    sounds.explosion.play()
                    cannon.score += 100
                bullets.remove(bullet)

Vi behöver också skriva ut poängen i spelfönstret. Lägg till en rad sist i speldelens draw-metod enligt nedan. Om du inte har laddat ned typsnittet Space Invaders, strunta i koden fontname="space_invaders".

def draw():
    screen.clear()
    cannon.draw()
  
    for bullet in bullets:
        bullet.draw()
    
    for alien in aliens:
        alien.draw()

    screen.draw.text("SCORE: %d" % cannon.score, (20, 20), fontname="space_invaders", fontsize=20)

Puh! Det var allt för den här gången. En hel del arbete återstår visserligen innan spelet är komplett. Till exempel är ju kanonen än så länge odödlig, så det går inte att förlora. Och så kan vi ju inte ha det. Men det och mycket annat ordnar vi i nästa, avslutande inlägg.

/Mats

PS. Om du har haft svårt att hänga med, eller får felmeddelanden och behöver kontrollera din egen kod mot ett ”facit”, så ligger hela källkoden här.

Annonser