5. Obstakels#
Op dit moment is onze Endless Runner nog geen spel. We hebben een speler die kan bewegen en een achtergrond, maar er is nog geen uitdaging. Daar gaan we nu verandering in brengen door obstakels toe te voegen. De speler moet deze obstakels ontwijken om zo lang mogelijk in leven te blijven.
Lijst met Actors#
Voor de obstakels gebruiken we Actor variabelen. Het probleem is echter dat we van tevoren niet weten hoeveel obstakels er tegelijkertijd in beeld zijn. Dus hoeveel Actor variabelen moeten we dan maken? Het antwoord is: we maken een lijst waaraan we Actor variabelen kunnen toevoegen als dat nodig is. We noemen onze lijst obstacles en bij aanvang van het spel is de lijst nog leeg. In het assignment statement in regel 22 zie je dat aan de twee vierkante haakjes waartussen niets staat:
13# Actors
14player = Actor('walk00')
15walk_images = ['walk00', 'walk01', 'walk02', 'walk03']
16player.images = walk_images
17player.fps = 10
18player.left = 10
19player.bottom = BASELINE
20player.vy = 0
21
22obstacles = []
Het toevoegen van een nieuw obstakel aan de lijst doen we in een aparte functie:
24# Functie add_obstacle()
25def add_obstacle():
26 obstacle = Actor('cactus00')
27 obstacle.x = WIDTH + 50
28 obstacle.bottom = BASELINE
29 obstacles.append(obstacle)
De functie add_obstacle() doet het volgende:
In regel 26 maken we een nieuwe
Actorvariabeleobstacleaan met de spritecactus00.png.In regel 27 plaatsen we het obstakel buiten het zichtbare deel van het venster, 50 pixels buiten de rechterrand.
In regel 28 plaatsen we het obstakel op de grond.
In regel 29 voegen we het obstakel toe aan de lijst
obstacles. Daarvoor gebruiken we de lijstmethodeappend(). Append is het Engelse woord voor toevoegen.
Om de nieuwe functie te testen, maken we een on_mouse_down() event handler die een nieuw obstakel toevoegt wanneer we met de muis klikken:
60# Event handler on_mouse_down()
61def on_mouse_down():
62 add_obstacle()
63 print(obstacles)
Met print(obstacles) zorgen we ervoor dat telkens wanneer een muisknop ingedrukt de huidige lijst met obstakels wordt afgedrukt in de console. De console is het venster onderaan in Mu Editor waarin je de uitvoer van je programma kunt zien. Houd deze dus goed in de gaten, terwijl je het programma runt en met de muis klikt.
Als het goed is, wordt nu elke keer dat je met de muis klikt, een nieuw Actor object toegevoegd aan de lijst. Later zullen we de on_mouse_down() event handler weer verwijderen, want dan moeten de obstakels automatisch worden toegevoegd. Maar om te testen of alles werkt, is dit een handige manier.
Obstakels tekenen en bewegen#
Nu we obstakels kunnen toevoegen aan de lijst, moeten we ze nog tekenen en laten bewegen. We doen dit in de draw() en update() functies met behulp van for loops. Voor de beweging van de obstakels voegen we ook een nieuwe constante SPEED toe.
8# Constanten
9HORIZON = 400
10BASELINE = HORIZON + 45
11GRAVITY = 1
12SPEED = 8
39# Functie draw()
40def draw():
41 draw_background()
42 player.draw()
43 for obstacle in obstacles:
44 obstacle.draw()
46# Functie update()
47def update():
48 player.animate()
49
50 player.y += player.vy
51
52 if player.bottom > BASELINE:
53 player.bottom = BASELINE
54 player.vy = 0
55 elif player.bottom < BASELINE:
56 player.vy += GRAVITY
57
58 for obstacle in obstacles:
59 obstacle.x -= SPEED
Run de code en klik met de muis. Zie je cacti (je mag ook cactussen zeggen hoor) verschijnen?
Memory leaks voorkomen#
Obstakels die links uit het beeld zijn verdwenen, moeten we eigenlijk weer uit de obstacles lijst verwijderen. Als we dat niet doen, wordt de lijst steeds voller en voller. Programmeurs noemen zoiets een memory leak: onnodig geheugengebruik. Laten we dat lek meteen dichten door de volgende regels aan de update() functie toe te voegen:
61 for obstacle in obstacles.copy():
62 if obstacle.right < 0:
63 obstacles.remove(obstacle)
64
65 print(obstacles)
In regel 61 staat iets bijzonders: obstacles.copy(). Daarmee maken we een kopie van de obstacles lijst. Met for gaan we langs alle obstakels in die lijst en als een obstakel links buiten beeld is geraakt, verwijderen we het uit de originele obstacles lijst. De aanroep print(obstacles) in regel 65 is tijdelijk toegevoegd zodat je in de console goed kunt zien hoe de lijst zich vult en weer leegt.
Vraag
Waarom maken we een kopie van de obstacles lijst? Waarom zouden we niet gewoon het volgende doen?
61 for obstacle in obstacles:
62 if obstacle.right < 0:
63 obstacles.remove(obstacle)
64
65 print(obstacles)
Antwoord
Wanneer je met een for loop door een lijst gaat, waaruit je tijdens die loop items verwijdert, werkt de loop niet meer goed. Laten we een voorbeeld bekijken met een lijst van strings:
1stringlist = ['A', 'B', 'B', 'A', 'A']
2
3print(f'Voor:\t{stringlist}')
4
5for s in stringlist:
6 if s == 'B':
7 stringlist.remove(s)
8
9print(f'Na:\t{stringlist}')
Vraag: wat zou het resultaat zijn van deze code?
Antwoord
Het resultaat is:
Voor: ['A', 'B', 'B', 'A', 'A']
Na: ['A', 'B', 'A', 'A']
Eerst wordt de volledige lijst afgedrukt, met daarin twee B’s. Na het verwijderen verwacht je dat er geen B’s meer in de lijst staan, maar dat is niet het geval. Hoe kan dat? Toen de eerste B uit de lijst werd verwijderd, schoven de resterende items in de lijst een plekje op. De tweede B kwam op de plek van de oude terecht, maar de for loop ging gewoon naar het volgende item en sloeg daardoor deze tweede B over!
Door een kopie van de lijst te maken, kunnen we de originele lijst gewoon aanpassen zonder dat dat invloed heeft op de for loop. De for loop gaat namelijk door de kopie van de lijst en niet door de originele lijst. Het resultaat is dat er geen B’s meer in de lijst staan.
1stringlist = ['A', 'B', 'B', 'A', 'A']
2
3print(f'Voor:\t{stringlist}')
4
5for s in stringlist.copy():
6 if s == 'B':
7 stringlist.remove(s)
8
9print(f'Na:\t{stringlist}')
Nu worden wel alle B’s uit de lijst verwijderd:
Voor: ['A', 'B', 'B', 'A', 'A']
Na: ['A', 'A', 'A']
Overigens is \t in regels 3 en 9 van dit voorbeeld een escape teken dat een tab maakt in de tekst. Dit is handig om de twee lijsten netjes recht onder elkaar uitgelijnd te krijgen.
Verwijder regel 65 weer uit je code. Hij was slechts bedoelt om te zien of de code goed werkte.
Obstakels automatisch spawnen#
Het toevoegen van nieuwe obstakels gebeurt nu met muisklikken, maar we willen natuurlijk dat er automatisch cacti verschijnen. Het verschijnen van nieuwe objecten in een game wordt ook wel spawnen genoemd. Voor het spawnen van onze obstakels hoeven we slechts een paar dingen te doen:
een variabele maken die de tijd aangeeft tot weer een nieuw obstakel moet verschijnen;
het toevoegen van nieuwe obstakels verwerken in de
update()functie.
De variabele voor de tijd noemen we obstacle_timeout:
23obstacles = []
24obstacle_timeout = 60
In de update() functie voegen we code toe die de waarde van obstacle_timeout telkens aflaagt met 1. Als de waarde nul (of eventueel kleiner) is geworden, wordt een nieuw obstakel toegevoegd en obstacle_timeout weer op 60 gezet. Omdat de update() functie 60 keer per seconde wordt uitgevoerd, zit er nu telkens precies een seconde tussen twee obstakels.
68 obstacle_timeout -= 1
69 if obstacle_timeout <= 0:
70 add_obstacle()
71 obstacle_timeout = 60
Heb je de code geprobeerd en een foutmelding gekregen? Dat klopt. We geven in de regels 68 en 71 namelijk obstacle_timeout met een assignment statement een nieuwe waarde, terwijl die variabele van buiten de update() functie komt.
Om dit op te lossen moet je aan het begin van de update() functie aangeven dat obstacle_timeout een zogenoemde globale variabele is: een variabele die buiten de functie is gecreëerd. Dat doe je met het global keyword:
47# Functie update()
48def update():
49 global obstacle_timeout
50
51 player.animate()
Nu werkt de code. Elke seconde verschijnt een nieuwe cactus.
Willekeurigheid inbouwen#
Het is natuurlijk niet zo leuk als de obstakels telkens met tussenpozen van precies 1 seconde verschijnen. Het is spannender wanneer er soms kort achter elkaar twee obstakels verschijnen en dan weer een tijdje niets. We gaan daarom de tijd tussen twee obstakels randomiseren, oftewel willekeurig maken. Ook gaan we de sprite van de cactus random kiezen uit de zes sprites die in de images map staan.
Om gebruik te maken van ‘random functies’ hebben we de module random nodig. Voeg daarom de volgende regel toe aan het begin van je code:
1from pgzhelper import *
2import random
De random module bevat een flink aantal functies die met willekeurigheid te maken hebben. Je vindt de documentatie van de module op de officiële Python website en voor een overzichtelijke lijst kun je kijken op www.w3schools.com.
Wij gebruiken voor de obstakels twee van de beschikbare functies:
random.randint(a, b): geeft een willekeurig geheel getal terug tussen de getallenaenb(inclusiefaenb).random.choice(sequence): geeft een willekeurig element terug uit desequence. Een lijst in Python is een voorbeeld van eensequence.
Een voorbeeld van het gebruik van die laatste functie:
import random
mylist = ['appel', 'banaan', 'citroen', 'druif']
print(random.choice(mylist))
In bovenstaande code wordt een willekeurige fruitsoort uit de lijst mylist gekozen en afgedrukt. Dit kan dus elke keer weer iets anders zijn.
Opdracht 01
In regel 72 staat op dit moment obstacle_timeout = 60. Vervang de waarde 60 door een willekeurig getal tussen 30 en 90. Gebruik hiervoor de random.randint() functie.
Opdracht 02
Voeg in regel 26 een nieuwe variabele obstacle_images toe die een lijst is van de zes cactus sprites:
24obstacles = []
25obstacle_timeout = 60
26obstacle_images = ['cactus00', 'cactus01', 'cactus02', 'cactus03', 'cactus04', 'cactus05']
Gebruik vervolgens in de functie add_obstacle() in regel 30 de random.choice() functie om een willekeurige sprite uit obstacle_images te kiezen voor het nieuwe obstakel.
Je code bevat nu nog steeds de on_mouse_down() event handler waarmee je met muisklikken cacti kunt laten verschijnen. Verwijder deze functie uit je code, want we hebben hem niet meer nodig.