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.
Event-handling skal håndtere input fra brukeren.
Update skal oppdatere spillobjektene våre.
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 skjermenscreen
, klokkenclock
, en ordbok over hvilke knapper som blir trykket påactions
, spillerenspiller
og en liste over fiendenefiender
. Den har også en hovedløkkemain_loop()
som kjører de forskjellige delene av spillet somhandle_events()
,update()
ogrender()
.handle_events()
er en metode som oppdatereractions
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\)-posisjonx
, en \(y\)-posisjony
, en breddewidth
, en høydeheight
, en RGB-tuppelcolor
ogrect
som er etRect
-objekt som oppdateres medupdate()
og tegnes medrender()
.Spiller
-klassen arver fraSpillObjekt
og har en egen farge og en fast bredde og høyde. Den har også en egenupdate()
-metode som gjør at man kan flytte på spilleren.Fiende
-klassen arver fraSpillObjekt
og har en egen farge og en fast bredde og høyde. Den har også en egenupdate()
-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å enRect
ved å brukeRect.collidepoint(point)
-metoden. Events medMOUSEBUTTON
har posisjonen til musepekeren pakket inn i attributtenevent.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, etBall
-objekt og flereBoks
-objekter.Ballen skal sprette av
Spiller
-objektet, kantene på skjermen (oppe, venstre, høyre) ogBoks
-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 etRø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.