Spill#

Å lage spill er en fin måte å øve seg på objektorientert programmering. For å lage spill kommer vi til å bruke pygame-pakken.

Du kan finne dokumentasjonen til Pygame på denne nettsiden.

Boilerplate#

Her er et enkelt eksempel som viser hvordan vi kan sette opp et spill med pygame.

import pygame

pygame.init()
screen = pygame.display.set_mode((500, 500))
clock = pygame.time.Clock()

# Main loop
running = True
while running:
    # Event-handling
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    
    # Update
    clock.tick(60)

    # Render
    screen.fill("white")
    pygame.display.flip()

pygame.quit()

Før hovedløkken initialiserer vi noen objekter. Hovedløkken kan tenkes på som tre deler som gjøres etter hverandre.

  1. Event-handling skal håndtere input fra brukeren.

  2. Update skal oppdatere spillobjektene våre.

  3. Render skal tegne spillobjektene på skjermen.

Kjører vi programmet får vi bare en hvit skjerm. La oss lage et spill ut av dette.

Et enkelt spill#

La oss lage et enkelt spill som viser noen grunnleggende elementer i pygame.

import pygame, random

class Spill:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((500, 500))
        self.clock = pygame.time.Clock()
        self.actions = {"left" : False, "right" : False, "up" : False, "down" : False}
        self.spiller = Spiller(self.screen.get_width() // 2, self.screen.get_height() // 2)
        self.fiender = [Fiende(random.randint(0, self.screen.get_width()), random.randint(0, self.screen.get_height())) for n in range(3)]
        self.running = True

    def main_loop(self):
        self.handle_events()
        self.update()
        self.render()

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_LEFT:
                    self.actions["left"] = True
                if event.key == pygame.K_RIGHT:
                    self.actions["right"] = True
                if event.key == pygame.K_UP:
                    self.actions["up"] = True
                if event.key == pygame.K_DOWN:
                    self.actions["down"] = True

            if event.type == pygame.KEYUP:
                if event.key == pygame.K_LEFT:
                    self.actions["left"] = False
                if event.key == pygame.K_RIGHT:
                    self.actions["right"] = False
                if event.key == pygame.K_UP:
                    self.actions["up"] = False
                if event.key == pygame.K_DOWN:
                    self.actions["down"] = False
    
    def update(self):
        self.spiller.update()
        for fiende in self.fiender:
            fiende.update()
        self.clock.tick(60)

    def render(self):
        self.screen.fill("white")
        for fiende in self.fiender:
            fiende.render()
        self.spiller.render()
        pygame.display.flip()

class SpillObjekt:
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.color = (100, 100, 100)
        self.rect = pygame.Rect(x - width // 2, y - height // 2, width, height)

    def update(self):
        self.rect = pygame.Rect(self.x - self.width // 2, self.y - self.height // 2, self.width, self.height)

    def render(self):
        pygame.draw.rect(spill.screen, self.color, self.rect)

class Spiller(SpillObjekt):
    def __init__(self, x, y):
        super().__init__(x, y, width = 10, height = 10)
        self.color = (0, 200, 0)

    def update(self):
        if spill.actions["left"]:
            self.x -= 5
        elif spill.actions["right"]:
            self.x += 5
        if spill.actions["up"]:
            self.y -= 5
        elif spill.actions["down"]:
            self.y += 5

        for fiende in spill.fiender:
            if pygame.Rect.colliderect(self.rect, fiende.rect):
                spill.running = False
        
        self.rect = pygame.Rect(self.x - self.width // 2, self.y - self.height // 2, self.width, self.height)

class Fiende(SpillObjekt):
    def __init__(self, x, y):
        super().__init__(x, y, width = 20, height = 20)
        self.color = (200, 0, 0)
        self.speed = 5
        self.vx = random.choice([-1, 1])
        self.vy = random.choice([-1, 1])

    def update(self):
        self.x += self.vx * self.speed
        self.y += self.vy * self.speed

        if self.x > spill.screen.get_width() or self.x < 0:
            self.vx *= -1
        if self.y > spill.screen.get_height() or self.y < 0:
            self.vy *= -1

        self.rect = pygame.Rect(self.x - self.width // 2, self.y - self.height // 2, self.width, self.height)


spill = Spill()
while spill.running:
    spill.main_loop()

Man kan utvide og forbedre dette spillet på mange måter, men det fungerer godt som et skjelett for å lage spill videre. Her er en forklaring for noen sentrale deler:

  • Spill-klassen håndterer overordnede ting som skjermen screen, klokken clock, en ordbok over hvilke knapper som blir trykket på actions, spilleren spiller og en liste over fiendene fiender. Den har også en hovedløkke main_loop() som kjører de forskjellige delene av spillet som handle_events(), update() og render().

    • handle_events() er en metode som oppdaterer actions avhengig av hvilke knapper som er trykket på.

    • update() er en metode hvor spillets logikk kjøres.

    • render() er en metode hvor spillets grafikk tegnes.

  • SpillObjekt-klassen er en klasse for objektene i spillet vårt. Alle spillobjekter har en \(x\)-posisjon x, en \(y\)-posisjon y, en bredde width, en høyde height, en RGB-tuppel color og rect som er et Rect-objekt som oppdateres med update() og tegnes med render().

  • Spiller-klassen arver fra SpillObjekt og har en egen farge og en fast bredde og høyde. Den har også en egen update()-metode som gjør at man kan flytte på spilleren.

  • Fiende-klassen arver fra SpillObjekt og har en egen farge og en fast bredde og høyde. Den har også en egen update()-metode som gjør at den flyr rundt på skjermen og spretter av kantene.

Her er et UML-diagram for spillet:

        ---
title: Klassediagram/UML
---
classDiagram
direction LR
    class Spill
    Spill : +Surface screen
    Spill : +Clock clock
    Spill : +dict actions
    Spill : +Spiller spiller
    Spill : +list[Fiende] fiender
    Spill : +bool running
    Spill : main_loop()
    Spill : handle_events()
    Spill : update()
    Spill : render()
    
    class SpillObjekt
    SpillObjekt : +float x
    SpillObjekt : +float y
    SpillObjekt : +tuple color
    SpillObjekt : +int width
    SpillObjekt : +int height
    SpillObjekt : +Rect rect
    SpillObjekt : update()
    SpillObjekt : render()
    
    class Spiller
    Spiller : +tuple color
    Spiller : +int width
    Spiller : +int height
    Spiller : update()
    
    class Fiende
    Fiende : +tuple color
    Fiende : +int width
    Fiende : +int height
    Fiende : update()
    
    SpillObjekt --o Spill
    Spiller --> SpillObjekt
    Fiende --> SpillObjekt
    

Tips: Tekst#

La oss legge til en metode i Spill-klassen som håndterer skriving av tekst.

Tekst trenger en en overflate å tegne på, en skrifttype, en farge og en posisjon hvor det skal tegnes.

La oss ta spillet vi laget over og legge til en timer som viser hvor lenge man har overlevd.

class Spill:
    def __init__(self):
        """
        Kode fra tidligere
        """
        self.font = pygame.font.Font(None, 32)
        self.start_tid = pygame.time.get_ticks()

    """
    Metoder fra tidligere
    """
    def draw_text(self, surface : pygame.Surface, string : str, font : pygame.font.Font, color : tuple, center : tuple):
        # Lager tekst. Andre parameter er anti-alias. Sett til True for glatt og fin tekst.
        text = font.render(string, False, color)
        # Henter rektangelet rundt teksten, med sentrum der man ønsker.
        text_rect = text.get_rect(center = center)
        # Setter teksten på overflaten man spesifiserte.
        surface.blit(text, text_rect)

    # Modifiserer render-metoden for å legge til teksten.
    def render(self):
            self.screen.fill("white")

            # Tegner tiden siden starttiden i sekunder på toppen av skjermen i midten.
            self.draw_text(self.screen, str(round((pygame.time.get_ticks() - self.start_time) / 1000)), self.font, (0, 0, 0), (self.screen.get_width() // 2, 10))
            
            for fiende in self.fiender:
                fiende.render()
            self.spiller.render()
            pygame.display.flip()

Tips: Trykke bare én gang#

Måten vi satt opp input i actions passer godt til å lage spill hvor man holder knapper, men det blir vanskelig å trykke på en knapp bare én gang.

La oss si at vi lager et spill hvor man skal kunne hoppe, men man må slippe knappen for å kunne hoppe igjen. Tenk Flappy Bird.

class Spill:
    def __init__(self):
        """
        Konstruktør
        """
        self.jump = False

    """
    Andre metoder
    """

    def handle_input(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False

            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_UP:
                    self.jump = True

            if event.type == pygame.KEYUP:
                if event.key == pygame.K_UP:
                    pass

class Fugl:
    """
    Konstruktør og metoder
    """

    def update(self):
        if spill.jump:
            self.y -= 10
            spill.jump = False

Når vi trykker ned knappen UP blir spill.jump == True, og fuglen hopper. For at fuglen skal kunne hoppe igjen må man slippe knappen og så trykke den ned igjen.

Ønsker man flere slike knapper kan man for eksempel lage en ny ordbok pressed_actions som håndterer knapper på denne måten i tillegg til actions som håndterer holding av knapper, men det mest elegante hadde vært å lage en egen klasse Action som har attributtene held og pressed. Å lage et sånt system overlates til leseren 👽

Tips: Musepekeren og klikking#

Vi ønsker ofte å kunne hente posisjonen til musepekeren og å kunne registrere trykk. La oss se på en liten demo:

import pygame, random

class Spill:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((500, 500))
        self.font = pygame.font.Font(None, 32)
        self.clock = pygame.time.Clock()
        self.running = True
        self.boks = Boks(250, 250, 50, 50)

    def main_loop(self):
        self.handle_events()
        self.update()
        self.render()

    def handle_events(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False

            if event.type == pygame.MOUSEBUTTONDOWN:
                if self.boks.rect.collidepoint(event.pos):
                    print("Du trykket på boksen!")

    def update(self):
        self.clock.tick(60)

    def render(self):
        self.screen.fill("white")

        # Viser boksen
        self.boks.render()

        # Viser tekst
        mouse_pos = pygame.mouse.get_pos()
        self.draw_text(self.screen, str(mouse_pos), self.font, (0, 0, 0), (250, 30))

        pygame.display.flip()

    def draw_text(self, surface : pygame.Surface, string : str, font : pygame.font.Font, color : tuple, center : tuple):
        text = font.render(string, False, color)
        text_rect = text.get_rect(center = center)
        surface.blit(text, text_rect)

class Boks:
    def __init__(self, x, y, bredde, høyde):
        self.rect = pygame.Rect((x - (bredde // 2), y - (høyde // 2)), (bredde, høyde))

    def render(self):
        pygame.draw.rect(spill.screen, (150, 150, 150), self.rect)

spill = Spill()
while spill.running:
    spill.main_loop()

Her er noen ting vi kan merke oss:

  • For å merke klikk kan vi bruke pygame.MOUSEBUTTONDOWN. Deretter kan vi sjekke om musens posisjon ligger oppå en Rect ved å bruke Rect.collidepoint(point)-metoden. Events med MOUSEBUTTON har posisjonen til musepekeren pakket inn i attributten event.pos.

  • For å hente musepekerens posisjon kontinuerlig kan vi bruke pygame.mouse.get_pos().

Oppgaver#

Oppgave: Arkanoid 🕹️

Lag din egen versjon av spillet Arkanoid.

  • Spillet skal ha et Spiller-objekt, et Ball-objekt og flere Boks-objekter.

  • Ballen skal sprette av Spiller-objektet, kantene på skjermen (oppe, venstre, høyre) og Boks-objektene.

  • Når ballen spretter av en boks, skal boksen fjernes fra spillet.

  • Når ballen går utenfor den nedre kanten av skjermen skal spillet avsluttes og "Game over!" skrives ut i terminalvinduet.

  • Når alle boksene er borte fra skjermen skal spillet avsluttes og "Du vant!" skrives ut i terminalvinduet.

Ekstra utvidelser:

  • Gjør at man ikke kan bevege spilleren utenfor skjermen.

  • Gjør at boksene har opp til tre liv (tilfeldig), og en spesifikk farge for hvert antall liv. Når antall liv når 0 så fjernes boksen.

Oppgave: Flappy Bird 🐦

Lag din egen versjon av spillet Flappy Bird.

  • Spillet skal ha et Fugl-objekt på midten av skjermen som faller.

    • Hvis man trykker på en hoppknapp skal den hoppe litt opp.

  • Det skal starte Rør-objekter ved tilfeldige steder på kanten av skjermen.

    • Det skal være et Rør-objekt på toppen av skjermen og et på bunnen av skjermen slik at man kan passere på midten.

  • Hvis Fugl treffer et Rør skal man tape.

  • Hvis Fugl faller under banen skal man tape.

  • Hver gang man passerer et par med Rør skal man få et poeng.

  • Poengene skal være synlige. Skriv de også ut i terminalen ved GAME OVER.