6. Uitbreidingen#

In principe is het spel klaar, maar uiteraard kun je er nog allerlei extra’s aan toevoegen, bijvoorbeeld:

  • verschillende soorten fruit gebruiken in plaats van alleen een appel;

  • verschillende stukken fruit tegelijk laten vallen in plaats van telkens slechts één;

  • objecten laten vallen die juist niet mogen worden opgevangen, zoals bommen;

  • handige upgrades laten vallen, bijvoorbeeld een breder mandje of een extra leven;

  • de valsnelheid variëren, bijvoorbeeld gelijdelijk laten toenemen om het moeilijker te maken;

  • de snelheid van mand verhogen naarmate langer op een pijltjestoets wordt gedrukt;

  • een explosie tonen wanneer het fruit naast de mand valt;

  • verschillende levels maken.

En uiteraard kun je het spel nog verfraaien met achtergrondafbeeldingen, muziek en geluiden.

In dit deel staan programmeertips voor de eerste twee van de zojuist genoemde uitbreidingen. Uitgangspunt is de code die je tot nu toe hebt gemaakt en waarvan je hieronder een complete weergave ziet.

fruitcatcher.py#
 1import random
 2
 3# Vensterinstellingen
 4WIDTH = 600
 5HEIGHT = 400
 6TITLE = 'Fruit Catcher'
 7MARGIN = 20
 8
 9# Variabelen voor score en levens
10score = 0
11lives = 3
12
13# Variabelen om de status van het spel bij te houden
14game_over = False
15game_started = False
16
17# Sprite voor het mandje
18basket = Actor('basket')
19basket.speed = 5
20
21# Sprite voor het fruit
22fruit = Actor('apple_red')
23fruit.speed = 5
24
25# Initialisatie mandje
26def init_basket():
27    basket.x = WIDTH // 2
28    basket.bottom = HEIGHT
29
30# Initialisatie fruit
31def init_fruit():
32    fruit.x = random.randint(0 + MARGIN, WIDTH - MARGIN)
33    fruit.bottom = -1
34
35# Functie draw_score() tekent de score
36def draw_score():
37    screen.draw.text(f'Score: {score}', topright=(580,20), width=360, fontname="boogaloo", fontsize=48, color="#DDDDDD", gcolor="#666666", owidth=1.5, ocolor="black", alpha=0.8)
38
39# Functie draw_lives() tekent de hartjes die de levens voorstellen
40def draw_lives():
41    for life in range(lives):
42        screen.blit('heart', (10 + 40*life, 10))
43
44# Draw() functie
45def draw():
46    screen.clear()
47
48    if not game_started:
49        screen.draw.text('Druk op de spatiebalk', center=(WIDTH/2, HEIGHT/2))
50        return
51
52    if game_over:
53        screen.blit('game_over', ((WIDTH-256)//2, (HEIGHT-170)//2))
54        return
55
56    fruit.draw()
57    basket.draw()
58    draw_score()
59    draw_lives()
60
61# Update() functie
62def update():
63    global score, lives, game_over, game_started
64
65    # Start game
66    if keyboard.space and not game_started:
67        game_started = True
68
69    # Exit de update() functie als de game nog niet is gestart of als het game over is
70    if not game_started or game_over:
71        return
72
73    # Keyboard events
74    if keyboard.left:
75        basket.x -= basket.speed
76    elif keyboard.right:
77        basket.x += basket.speed
78    if basket.right > WIDTH:
79        basket.right = WIDTH
80    if basket.left < 0:
81        basket.left = 0
82
83    # Beweeg fruit
84    fruit.y += fruit.speed
85
86    # Collision detection
87    if fruit.top > basket.top:
88        if basket.collidepoint(fruit.center):
89            score += 1
90        else:
91            lives -= 1
92            if lives <= 0:
93                game_over = True
94        init_fruit()
95
96# HOOFDPROGRAMMA
97init_basket()
98init_fruit()

Verschillende fruitsoorten#

Download het zip-bestand fruit_sprites.zip. Een zip-bestand is een bestand waarin weer andere bestanden verpakt zijn. Je ziet in de Verkenner dat Windows dit bestand een Compressed (zipped) Folder noemt.

../_images/extra_different_fruits_01.png

Als je in Windows het bestand opent, lijkt het ook net alsof je een map hebt geopend.

../_images/extra_different_fruits_02.png

Sleep alle afbeeldingen vanuit het zip-bestand naar de fruitcatcher\images folder:

../_images/extra_different_fruits_03.png

We gaan voor de verschillende fruitsoorten niet verschillende Actor variabelen aanmaken. We gebruiken de variabele fruit die we al hadden en veranderen alleen maar de afbeelding ervan, telkens wanneer een nieuw stuk fruit valt. Daartoe maken we eerst een lijst variabele FRUIT_IMAGES aan met de namen van alle fruit afbeeldingen:

fruitcatcher.py#
 3# Vensterinstellingen
 4WIDTH = 600
 5HEIGHT = 400
 6TITLE = 'Fruit Catcher'
 7MARGIN = 20
 8
 9FRUIT_IMAGES = ['apple_green', 'apple_red', 'apple_yellow', 'banana', 'berry', 'cherry', 'lemon', 'lime', 'orange', 'pear', 'plum', 'watermelon']
10
11# Variabelen voor score en levens
12score = 0
13lives = 3

Om willekeurig een fruitafbeelding te kiezen, gebruiken we de random.choice() functie van de random module, die we toch al hadden geïmporteerd. We hoeven slechts de volgende regel toe te voegen aan de init_fruit() functie om het te laten werken:

fruitcatcher.py#
32# Initialisatie fruit
33def init_fruit():
34    fruit.image = random.choice(FRUIT_IMAGES)
35    fruit.x = random.randint(0 + MARGIN, WIDTH - MARGIN)
36    fruit.bottom = -1

Elke Actor in Pygame Zero heeft een image variabele. De waarde van die variabele is de naam van de afbeelding die moet worden getekend. In regel 34 vullen we de fruit.image variabele met een willekeurige naam uit de lijst FRUIT_IMAGES.

Verschillende stukken fruit#

Het spel wordt uitdagender als er meerdere stukken fruit tegelijk naar beneden vallen.

../_images/multiple_fruits.png

Wat echter ook uitdagender wordt is het programmeerwerk, want om dit voor elkaar te krijgen gaan we een lijstvariabele gebruiken. Dat deden we in de uitbreiding hiervoor ook al, maar dat was nog relatief eenvoudig.

Lijsten in Python#

In Python maak je een lijstvariabele door rechte haken te gebruiken:

>>> mijn_lijst = ['boter', 'kaas', 'eieren']

In dit voorbeeld is mijn_lijst een lijst met stringwaarden, maar je mag allerlei datatypes door elkaar gebruiken in een lijst:

>>> mijn_lijst = ['A', 100, True, 3.1415, 'Fabiola']

Je haalt een item uit een lijst op door zijn positie in de lijst in te voeren tussen vierkante haken. Deze positie noemen we de index positie. Het eerste item in een lijst heeft altijd index 0.

>>> mijn_lijst = ['A', 100, True, 3.1415, 'Fabiola']
>>> mijn_lijst[0]
'A'
>>> mijn_lijst[2]
True

In dit voorbeeld heeft het item 'Fabiola' index 4, maar je kunt in een lijst ook van achter naar voor tellen met negatieve indices. Zo heeft het item 'Fabiola' óók index -1:

>>> mijn_lijst = ['A', 100, True, 3.1415, 'Fabiola']
>>> mijn_lijst[4]
'Fabiola'
>>> mijn_lijst[-1]
'Fabiola'

Je kunt het aantal items in een lijst opvragen met de len() functie:

>>> mijn_lijst = ['A', 100, True, 3.1415, 'Fabiola']
>>> len(mijn_lijst)
5

Een item in een lijst wijzigen is heel eenvoudig:

>>> mijn_lijst = ['A', 100, True, 3.1415, 'Fabiola']
>>> mijn_lijst
['A', 100, True, 3.1415, 'Fabiola']
>>> mijn_lijst[2] = False
>>> mijn_lijst
['A', 100, False, 3.1415, 'Fabiola']

Met de .append() functie, kun je een item toevoegen aan een lijst:

>>> letters = ['A', 'B', 'C']
>>> letters.append('D')
>>> letters
['A', 'B', 'C', 'D']

En met de .remove() functie, verwijder je een item uit een lijst:

>>> letters = ['A', 'B', 'C']
>>> letters.remove('B')
>>> letters
['A', 'C']

Met een for loop kun je alle items in een lijst langslopen:

Code#
1letters = ['A', 'B', 'C']
2for letter in letters:
3    print(letter)
Output#
A
B
C

Lijst met fruit#

Om in Fruit Catcher meerdere stukken fruit te laten vallen, gebruiken we in plaats van de huidige fruit Actor een lijst van Actors. Om te beginnen vervangen we de fruit Actor variabele door een lege fruits lijst:

Oude code#
19# Sprite voor het mandje
20basket = Actor('basket')
21basket.speed = 5
22
23# Sprite voor het fruit
24fruit = Actor('apple_red')
25fruit.speed = 5
26
27# Initialisatie mandje
28def init_basket():
29    basket.x = WIDTH // 2
30    basket.bottom = HEIGHT
Nieuwe code#
19# Sprite voor het mandje
20basket = Actor('basket')
21basket.speed = 5
22
23# Lijst voor fruit Actors
24fruits = []
25
26# Initialisatie mandje
27def init_basket():
28    basket.x = WIDTH // 2
29    basket.bottom = HEIGHT

Vervolgens vervangen we de init_fruit() functie door een add_new_fruit_to_list() functie, die een nieuwe Actor aanmaakt, de snelheid, afbeelding en positie instelt en het fruit toevoegt aan de fruits lijst:

Oude code#
31# Initialisatie fruit
32def init_fruit():
33    fruit.image = random.choice(FRUIT_IMAGES)
34    fruit.x = random.randint(0 + MARGIN, WIDTH - MARGIN)
35    fruit.bottom = -1
Nieuwe code#
31# Voeg nieuw stuk fruit toe aan de lijst
32def add_new_fruit_to_list():
33    fruit = Actor('apple_red')
34    fruit.speed = random.randint(2, 4)
35    fruit.image = random.choice(FRUIT_IMAGES)
36    fruit.x = random.randint(0 + MARGIN, WIDTH - MARGIN)
37    fruit.bottom = -1
38    fruits.append(fruit)

Op regel 34 krijgt fruit.speed een random waarde, waardoor de stukken fruit met verschillende snelheden zullen vallen.

In de draw() functie moeten alle items in de fruits lijst worden getekend. Dat kan eenvoudig met een for loop:

Oude code#
49# Draw() functie
50def draw():
51    screen.clear()
52
53    if not game_started:
54        screen.draw.text('Druk op de spatiebalk', center=(WIDTH/2, HEIGHT/2))
55        return
56
57    if game_over:
58        screen.blit('game_over', ((WIDTH-256)//2, (HEIGHT-170)//2))
59        return
60
61    fruit.draw()
62    basket.draw()
63    draw_score()
64    draw_lives()
Nieuwe code#
49# Draw() functie
50def draw():
51    screen.clear()
52
53    if not game_started:
54        screen.draw.text('Druk op de spatiebalk', center=(WIDTH/2, HEIGHT/2))
55        return
56
57    if game_over:
58        screen.blit('game_over', ((WIDTH-256)//2, (HEIGHT-170)//2))
59        return
60
61    for fruit in fruits:
62        fruit.draw()
63    basket.draw()
64    draw_score()
65    draw_lives()

Uiteraard moeten we ook de update() functie aanpassen. Alle fruit items in de fruits lijst moeten naar beneden vallen en van elk item moeten we checken of het in het mandje terechtkomt:

Oude code#
 67# Update() functie
 68def update():
 69    global score, lives, game_over, game_started
 70
 71    # Start game
 72    if keyboard.space and not game_started:
 73        game_started = True
 74
 75    # Exit de update() functie als de game nog niet is gestart of als het game over is
 76    if not game_started or game_over:
 77        return
 78
 79    # Keyboard events
 80    if keyboard.left:
 81        basket.x -= basket.speed
 82    elif keyboard.right:
 83        basket.x += basket.speed
 84    if basket.right > WIDTH:
 85        basket.right = WIDTH
 86    if basket.left < 0:
 87        basket.left = 0
 88
 89    # Beweeg fruit
 90    fruit.y += fruit.speed
 91
 92    # Collision detection
 93    if fruit.top > basket.top:
 94        if basket.collidepoint(fruit.center):
 95            score += 1
 96        else:
 97            lives -= 1
 98            if lives <= 0:
 99                game_over = True
100        init_fruit()
Nieuwe code#
 67# Update() functie
 68def update():
 69    global score, lives, game_over, game_started
 70
 71    # Start game
 72    if keyboard.space and not game_started:
 73        game_started = True
 74
 75    # Exit de update() functie als de game nog niet is gestart of als het game over is
 76    if not game_started or game_over:
 77        return
 78
 79    # Keyboard events
 80    if keyboard.left:
 81        basket.x -= basket.speed
 82    elif keyboard.right:
 83        basket.x += basket.speed
 84    if basket.right > WIDTH:
 85        basket.right = WIDTH
 86    if basket.left < 0:
 87        basket.left = 0
 88
 89    # Beweeg fruit
 90    for fruit in fruits:
 91        fruit.y += fruit.speed
 92
 93    # Collision detection
 94    for fruit in fruits:
 95        if fruit.top > basket.top:
 96            if basket.collidepoint(fruit.center):
 97                score += 1
 98            else:
 99                lives -= 1
100                if lives <= 0:
101                    game_over = True
102            fruits.remove(fruit)
103            add_new_fruit_to_list()

De nieuwe code verschilt niet veel van de oude, maar let op dat de init_fruit() functie die we in de oude code in regel 100 aanriepen niet meer bestaat. In plaats daarvan gebruiken we fruits.remove(fruit) om de fruit Actor uit de lijst te verwijderen en direct daarna add_new_fruit_to_list() om een nieuwe fruit Actor te maken en in de lijst te zetten.

Als je op dit punt bent aangekomen, kun je je code testen. Wanneer je dat doet, zul je merken dat er ogenschijnlijk niks is veranderd. Er valt telkens maar één stuk fruit naar beneden. Je hebt echter nog maar één regel code nodig om heel veel fruit te laten vallen. Voeg onderaan de update() functie de aanroep add_new_fruit_to_list() toe. Maak daarna van regel 99 commentaar om te voorkomen dat het spel meteen is afgelopen.

Oude code#
 93    # Collision detection
 94    for fruit in fruits:
 95        if fruit.top > basket.top:
 96            if basket.collidepoint(fruit.center):
 97                score += 1
 98            else:
 99                lives -= 1
100                if lives <= 0:
101                    game_over = True
102            fruits.remove(fruit)
103            add_new_fruit_to_list()
Nieuwe code#
 93    # Collision detection
 94    for fruit in fruits:
 95        if fruit.top > basket.top:
 96            if basket.collidepoint(fruit.center):
 97                score += 1
 98            else:
 99                #lives -= 1
100                if lives <= 0:
101                    game_over = True
102            fruits.remove(fruit)
103            add_new_fruit_to_list()
104
105    add_new_fruit_to_list()

Run je code en geniet maar even van de ‘hoorn des overvloeds’.

../_images/too_many_fruits.png

Nu valt er veel te veel fruit om de game speelbaar te laten zijn. Het ziet er mooi uit, maar voor het spel is het niet zo handig.

Door in regel 105 add_new_fruit_to_list() aan te roepen wordt 60 keer per seconde een nieuw stuk fruit toegevoegd. De update() functie wordt immers 60 keer per seconde uitgevoerd. Het is beter om een nieuw stuk fruit toe te voegen zodra aan een bepaalde voorwaarde is voldaan:

105    if ...:
106        add_new_fruit_to_list()

Maar wat voor voorwaarde moet dat zijn? Je zou kunnen kiezen voor een tijdvoorwaarde, bijvoorbeeld elke 3 seconden een stuk fruit aan de lijst toevoegen. Omdat het iets gemakkelijker te programmeren is, kiezen we hier voor een positievoorwaarde: zodra het laatste stuk fruit in de lijst onder een denkbeeldige lijn komt, voegen we een nieuw stuk fruit toe.

../_images/treshold.png

In de figuur hierboven zie je de denkbeeldige drempel (in het engels treshold) getekend. De peer rechtsboven gaat juist over de drempel, en op dat moment zou een nieuw stuk fruit moeten worden gemaakt. Om dit te programmeren maken we eerst een treshold variabele:

fruitcatcher.py#
23# Lijst voor fruit Actors
24fruits = []
25
26# Drempelwaarde voordat nieuw fruit valt
27treshold = 0.25 * HEIGHT
28
29# Initialisatie mandje
30def init_basket():
31    basket.x = WIDTH // 2
32    basket.bottom = HEIGHT

In regel 27 geven we treshold de waarde 0.25*HEIGHT. Daardoor komt de denkbeeldige lijn op een kwart van de bovenkant van het venster te liggen.

../_images/treshold_position.png

Met het volgende if statement kunnen we er in de update() functie voor zorgen dat een nieuwe stuk fruit aan de lijst wordt toegevoegd zodra laatste stuk fruit in de lijst (met index -1) onder de drempellijn komt:

fruitcatcher.py#
 96    # Collision detection
 97    for fruit in fruits:
 98        if fruit.top > basket.top:
 99            if basket.collidepoint(fruit.center):
100                score += 1
101            else:
102                lives -= 1
103                if lives <= 0:
104                    game_over = True
105            fruits.remove(fruit)
106
107    # Check of nieuw fruit moet vallen
108    if fruits[-1].y > treshold:
109        add_new_fruit_to_list()

Merk op dat de aanroep add_new_fruit_to_list() die direct onder fruits.remove(fruit) (in regel 105) stond, is verwijderd. En in regel 102 is de # die we eerder plaatsten om te kunnen testen weggehaald.

Je ziet dat het gebruik van lijstvariabelen heel krachtig is. Ze maken het mogelijk om een stukje code toe te passen op een hele verzameling sprites in plaats van slechts één.