Archives for posts with tag: Pygame

Jag tänkte följa upp mitt förra inlägg med en liten tutorial om hur man kan skapa ett simpelt 2D-spel, en förenklad version av det klassiska arkadspelet Space Invaders, med Python och det nybörjarvänliga ramverket Pygame Zero. I den här första delen ska vi skapa ett spelfönster och förbereda oss för rymdinvasionen genom att se till så att vi har en kanon som vi kan styra och skjuta med. Tanken är att även den som har ytterst begränsade kunskaper om programmering ska kunna hänga med. Jag förutsätter dock att du har installerat Python 3, Pygame och Pygame Zero samt att du vet hur man skriver ett program i en texteditor och kör det via terminalen/kommandofönstret.

Det här med spelfönstret är snabbt avklarat. Du behöver bara skapa en tom fil och spara den som exempelvis ”game.py” och sedan köra den med kommandot pgzrun game.py. Ett ögonblick senare öppnas ett färdigt spelfönster! Men dimensionerna lämpar sig inte riktigt för det spel vi ska skapa. Och så skulle det vara trevligt om det stod något annat än ”Pygame Zero Game” längst upp i fönstret. Gå tillbaka till din texteditor och lägg till följande rader i ”game.py”:

WIDTH = 480
HEIGHT = 600
TITLE = '---=== SPACE INVADERS ===---'

Om du nu sparar och kör programmet igen så öppnas ett mer långsmalt fönster med vår egen text i titelraden. Men för att vi ska kunna uträtta något i vårt spel så måste vi också lägga till två metoder som uppdaterar och ritar de olika objekten i spelet. De här metoderna ska heta just update respektive draw och Pygame Zero kommer att se till så att de anropas, det vill säga att koden i dem körs, ungefär 60 gånger i sekunden. Vi gör följande lilla tillägg efter de tre första raderna:

# Här börjar själva spelet

def update():
    pass

def draw():
    pass

Den första raden, den som börjar med ”#”, är en kommentar. Den har ingen som helst betydelse för körningen av programmet men gör det lite lättare att tolka och hitta i koden. Sedan följer definitionerna av våra metoder. Nyckelordet pass betyder att metoderna än så länge inte utför något alls. Vi ska snart ersätta pass med betydligt intressantare kod. Först är det dock viktigt att du noterar att ordet pass i både update och draw är indraget med exakt fyra mellanslag. Det här med indrag, eller indentering, är oerhört viktigt i Python eftersom indraget avgör vilken kod som hör till vilket block. Så se till att även indragen finns med i ditt eget program.

För att kunna lägga till det första objektet i spelet, en kanon, så behöver vi skapa en klass. Om du inte är bekant med terminologin inom objektorienterad programmering så är en klass en beskrivning som talar om för datorn vilka egenskaper och förmågor ett objekt ska ha. När objektet ska skapas så följer datorn beskrivningen i klassen, ungefär som när en snickare följer en ritning. Vår första klass ska heta Cannon. Lägg till dessa rader ovanför raderna med metoderna update och draw, precis under raden där vi ger spelet en titel:

# Här nedan är de klasser som används i spelet

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

Tycker du att koden ser konstig ut? Oroa dig inte, det tycker jag också. Python är känt för sin läsbara kod, men när man använder objektorientering så blir det lätt ganska rörigt. På första raden efter den inledande kommentaren talar vi om att klassen ska heta Cannon och att den ska ärva, som det heter, från klassen Actor, en fördefinierad klass som ingår i Pygame Zero. När en klass ärver från en annan klass så får den samma egenskaper och förmågor, utöver de egna, som ”föräldern” eller superklassen som man oftast kallar den. På de två följande raderna definieras metoden __init__  (ja, det ska vara två understreck både före och efter ordet ”init”) som är en speciell metod som körs automatiskt när ett objekt av klassen konstrueras. Där kan man initiera objektet, t ex tala om vilken startposition det ska ha i spelfönstret. Det enda som i nuläget sker i vår init-metod är att vi anropar superklassens, det vill säga Actors, init-metod.

Okej. För att vår kanon ska visas på skärmen behöver vi lägga till ytterligare lite kod. Men först behöver du ladda ner den lilla bilden nedan. Skapa en ny katalog kallad ”images” i den katalog där ditt program ligger och spara bildfilen som ”cannon.png”.  Obs att det är viktigt att filen heter just så, och att den ligger i en katalog som heter ”images”.

cannon
När du har sparat bildfilen på rätt ställe så behöver du uppdatera ”spel-delen” av koden på följande vis:

# Här börjar själva spelet

cannon = Cannon('cannon', (WIDTH / 2, 560))

def update():
    pass

def draw():
    screen.clear()
    cannon.draw()

Här skapar vi ett objekt, eller en instans, av klassen Cannon och lagrar den i en variabel som heter just cannon. När vi skapar kanonen så skickar vi med namnet på den sprite som ska användas (ändelsen ”.png” behövs inte här) samt kanonens startposition. För att få positionen i sidled, eller x-led, så delar vi spelfönstrets bredd med 2, vilket blir 240. Kanonen kommer alltså att ritas 240 pixlar åt höger från fönstrets vänstra kant. Positionen i höjdled, eller y-led, sätts till 560, det vill säga 560 pixlar nedåt från fönstrets överkant. I metoden draw har två rader lagts till som först rensar fönstret på innehåll och sedan ritar ut kanonen. Spara och kör programmet igen och – voilà! – kanonen visas i fönstrets nedre del.

Än så länge är kanonen helt orörlig, men det är lätt åtgärdat. Först lägger vi till följande kod i metoden update:

def update():
    if keyboard.right:
        cannon.move_right()
    elif keyboard.left:
        cannon.move_left()

Det här är en så kallad if-sats som säger att om höger piltangent är nedtryckt så ska kanonens metod move_right anropas. Annars, om vänsterpilen är nedtryckt, så ska move_left anropas. För att det här ska funka så måste vi också lägga till de här metoderna i klassen Cannon:

class Cannon(Actor):
    def __init__(self, sprite, position):
        super(Cannon, self).__init__(sprite, position)
        self.speed = 5

    def move_right(self):
        self.x += self.speed
        if self.right >= WIDTH - 40:
            self.right = WIDTH - 40
    
    def move_left(self):
        self.x -= self.speed
        if self.left <= 40:
            self.left = 40

I koden ovan har även raden self.speed = 5 lagts till i __init__. Den talar om med vilken hastighet kanonen ska röra sig. I metoderna move_right och move_left så används hastigheten för att flytta kanonens x-position åt höger respektive vänster, med 5 pixlar åt gången. Fundera gärna en stund på hur if-satserna i metoderna fungerar. De gör så att kanonen alltid stoppas 40 pixlar från fönstrets kanter.

Nu kan vi styra kanonen. Men den måste ju kunna skjuta också, annars är det inte mycket till kanon. Vi behöver skapa en ny klass, som vi kan kalla för Bullet och som representerar just själva ”kulorna” eller skotten från kanonen. Lägg till följande kod, direkt efter klassen Cannon:

class Bullet(Actor):
    def __init__(self, sprite, position):
        super(Bullet, self).__init__(sprite, position)
        self.speed = 20
  
    def update(self):
        self.y -= self.speed

Som du ser så har skotten en betydligt högre hastighet än vår kanon. Ett skott ska inte gå att styra, utan när det har avfyrats så ska det röra sig uppåt automatiskt. Det ordnas i metoden update, där skottets y-position minskas med 20 pixlar varje gång metoden anropas. Vi behöver också göra flera tillägg i koden efter klassdefinitionerna:

# Här startar själva spelet

cannon = Cannon('cannon', (WIDTH / 2, 560))
bullets = []    
  
def update():
    if keyboard.right:
        cannon.move_right()
    elif keyboard.left:
        cannon.move_left()
  
    if keyboard.space:
        bullets.append(Bullet('bullet', cannon.pos))
      
    for bullet in bullets:
        bullet.update()
    
    
def draw():
    screen.clear()
    cannon.draw()
  
    for bullet in bullets:
        bullet.draw()

Här finns en hel del att förklara. För det första vill vi inte ha ett enda skott, utan vi vill kunna hantera en hel drös med skott på samma gång. Därför så skapar vi, med koden bullets = [], en lista där vi kan lägga till och ta bort skott lite som vi vill. I metoden update ser vi till att ett nytt skott skapas och lagras i listan varje gång spelaren trycker på space-tangenten. Och i både update och draw så går vi igenom hela listan med skott med hjälp av så kallade for-loopar, och ser till så att vart och ett av skotten uppdateras och ritas ut på skärmen. Verkar for-looparna skumma? Tänk dig att du i stället skulle ge datorn muntliga instruktioner. Då skulle du säga något i stil med: ”För varje kula i listan med kulor, uppdatera/rita kulan.” Och om du provar att översätta koden så ser du att det är ju precis vad den säger.

Åh, just det. Du måste också ladda ner den pyttelilla skott-bilden nedan och spara den i ”images” som ”bullet.png”.

bullet

Ta sedan och spara och provkör programmet.

space_invaders

Wow, vilken kulsvärm! Rymdvarelserna kommer inte ha en chans! Fast… det kanske inte blir så kul? Spel ska ju helst vara lagom svåra för att man inte ska tröttna direkt. Vi får ta och sätta ett tak för hur många skott per sekund som kanonen kan avfyra. Vi ska också se till så att skott som är ”döda”, det vill säga har hamnat utanför fönstret, plockas bort ur listan. Annars kommer de där for-looparna, där listan med skott gås igenom, att ta onödigt lång tid när man har spelat ett tag och avfyrat några tusen skott.

För att kunna mäta hur lång tid det har gått sedan ett skott fyrades av så behöver vi importera en färdig metod från Pygame-biblioteket. Den här metoden heter get_ticks och varje gång som den anropas så talar den om hur många millisekunder, alltså tusendels sekunder, som har förflutit sedan spelet startades. Lägg till denna rad allra överst i programmet:

from pygame.time import get_ticks

Vidare så behöver vi ett par nya variabler i klassen Cannon

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

… och så behöver vi lägga till metoden is_dead i klassen Bullet:

class Bullet(Actor):
    def __init__(self, sprite, position):
        super(Bullet, self).__init__(sprite, position)
        self.speed = 20
  
    def update(self):
        self.y -= self.speed
    
    def is_dead(self):
        return self.bottom <= 0

Till sist så måste vi lägga till lite kod i spel-delen, i metoden update:

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)

I den nya if-satsen under if keyboard.space så kontrolleras nu om det har gått mer än 300 millisekunder innan det förra skottet avfyrades. Om så är fallet så avfyras ett nytt skott och tidpunkten lagras i variabeln cannon.last_fire. Observera att det egentligen inte ska vara någon radbrytning i den rad som börjar med if get_ticks(), utan hela villkoret i if-satsen ska stå på en och samma rad. Men här på bloggen fick all kod inte plats. Lägg också märke till den sista if-satsen, som kontrollerar om kulan är ”död”. Man kan se det som att spelet frågar kulan om den har hamnat utanför fönstret. Om så är fallet, om dess nederkant har en y-position som är mindre än 0, så svarar metoden is_dead genom att returnera värdet True. I annat fall så svarar den False.

Sådär, nu är vi redo för själva rymdinvasionen! Fast den får vänta till nästa blogginlägg. Tills dess så får du som har frågor eller synpunkter gärna höra av dig, antingen i kommentarsfältet nedan eller på Twitter.

/Mats

Häromdagen stötte jag på ett riktigt trevligt projekt som jag känner att jag måste tipsa om: Pygame Zero. Det är ett litet ramverk som bygger på Pygame, ett spelbibliotek som används för att skapa spel i Python, och som främst är tänkt att användas för undervisning. Som upphovsmannen Daniel Pope själv beskriver det: Pygame Zero gör det möjligt för lärare att lära ut programmeringskoncept utan att först behöva förklara saker som spelloopar och händelseköer. De här sakerna, och mycket annat, är dolda för den som använder ramverket.

Jag har lekt lite grann med Pygame Zero och kan konstatera att det på många sätt liknar Gosu, det ramverk för spelprogrammering i Ruby som jag har tjatat om i många tidigare inlägg här på bloggen. Men Pygame Zero tar det här med att abstrahera bort ”krångliga” saker ytterligare några steg längre. Till exempel: För att öppna ett grafikfönster så behöver du inte skriva en enda klass. Faktum är att du inte behöver skriva någon kod alls. Du kan bara skapa en tom fil, döpa den till t ex ”game.py” och köra den med ett speciellt program som ingår i ramverket. Ett fönster med en rityta på 800 gånger 600 pixlar framträder på skärmen.

Skärmbild_2015-08-05_18-33-43

Är det här bra eller dåligt, att så pass mycket sker i bakgrunden utan att användaren kommer i kontakt med koden? Det är en fråga som man kan vrida och vända på länge. Den som på allvar vill lära sig att programmera i Python måste förstås, förr eller senare, även greppa saker som import av moduler och iteration över listor. Men det kanske inte är det allra första man behöver lära sig. Jag är inte pedagog, men jag tror att det är viktigt att programmeringen snabbt ger roliga resultat för att inte motivationen hos eleven ska tryta. Och en rymdvarelse som svävar i ett grafikfönster är definitivt roligare än en while-loop. Har du en åsikt i frågan, eller andra synpunkter, hör gärna av dig i kommentarsfältet nedan.

För att kunna använda Pygame Zero måste du först installera Python 3 och Pygame. Installationsanvisningar hittar du här. Jag har utan större problem installerat ramverket på tre olika datorer: En med Xubuntu 15.04, en med Windows 8.1 och en Raspberry Pi med Raspbian. I skrivande stund har gänget bakom Pygame Zero precis släppt en ny version, som förutom buggfixar och nya funktioner innehåller ett knippe exempelspel i form av implementationer av klassiska spel som Snake, Pong och Lunar Lander.

Själv tänker jag försöka skriva om en liten Space Invaders-klon som jag en gång kodade i Ruby till Python och använda Pygame Zero. Min förhoppning är att detta projekt så småningom ska mynna ut i en liten tutorial. Den som lever får se.

/Mats