[P5] Project: Bellenschieter Vereiste voorkennis: fiches [7] [8] [9] [11] [12] [13] [14] [18] (dit programma is sterk geïnspireerd door het boek “Programmeren voor kinderen” van Carol Vorderman)
Opdracht We gaan een grafisch spelletje maken, waarbij we met een duikboot onder water varen en luchtbellen moeten kapot prikken. We werken niet met sprites – dat zijn tekeningen die we importeren en gebruiken – maar we gaan alles zelf tekenen, met de grafische bibliotheek tkinter. We werken dus met vereenvoudigde voorstellingen voor bv. de duikboot. Om het spel te spelen is er een beperkte tijd. Na één minuut is het spel afgelopen en krijg je een score. Wie meer dan 1000 punten scoort binnen de minuut krijgt bonustijd en mag ietsje langer spelen.
Wat hebben we nodig? -
De grafische bibliotheek tkinter, om onze vormen te tekenen. Een apart venster waarin we onze grafische elementen laten verschijnen. Een aftelklok. We gebruiken daarvoor de bibliotheek time. De besturing van het spel loopt via de pijltjestoetsen. We moeten deze toetsaanslagen dus opvangen in ons programma. Op het einde geven we een score. Die moeten we dus bijhouden in de loop van het programma. We werken met vereenvoudigde vormen. Dit wordt onze duikboot:
Aan de slag We beginnen met het importeren van de nodige bibliotheken: import import import import
tkinter random time math
We gebruiken -
tkinter: om te tekenen random: om willekeurige getallen te genereren time: om met tijdsfuncties te werken math: om geavanceerde wiskundige bewerkingen te gebruiken.
De hoogte en breedte van ons venster gaan we in constanten gaan definiëren. Door deze constanten aan te passen kunnen we dus gemakkelijk de grootte van ons spel aanpassen. Python [P5] Project: Bellenschieter
versie 9/03/2015 1/11
HOOGTE = 500 BREEDTE = 800 Nu gaan we het venster in kwestie gaan maken. Dat doen we met de Tk() functie uit de tkinter-bibliotheek. venster = tkinter.Tk() We geven een titel aan het venster: venster.title('Bellenschieter') En nu gebruiken we een speciale klasse Canvas() uit de tkinter-bibliotheek om het eigenlijke venster te tekenen. We geven hierbij de breedte en hoogte op, en geven via de bg parameter ook al direct op dat we het venster donkerblauw willen opvullen (darkblue). c = tkinter.Canvas(venster, width=BREEDTE, height=HOOGTE, bg='darkblue') c.pack() Nu gaan we onze duikboot tekenen. De duikboot bestaat uit een driehoek (een veelhoek of polygoon met drie zijden) en een cirkel (een ovaal waarvan de hoogte gelijk is aan de breedte. De functies create_polygon() en create_oval() kunnen binnen een Canvas() object gebruikt worden om zo’n vormen te tekenen. Om uit te leggen hoe er binnen een venster met coördinaten wordt gewerkt, bekijken we even een tekening.
Je ziet een horizontale as (x-as) en een verticale as (y-as). De linkerbovenhoek van ons tekenvenster is het nulpunt. De negatieve waarden vallen bij ons dus “buiten beeld”. Om een cirkel te tekenen met als middelpunt (x=3, y=2) en straal 2 moeten we het commando create_oval(2, 1, 4, 3) gebruiken. Daarmee geven we de coördinaten op van de uiterste hoekpunten Python [P5] Project: Bellenschieter
versie 9/03/2015 2/11
van een rechthoek (het groene punt is (2, 1), het oranje punt is (4, 3). Binnen die rechthoek wordt dan een ovaal getekend – in ons geval een perfecte cirkel. Bij create_polygon() werken we nog wat anders: daar geven we de coördinaten op van de drie verschillende hoeken van onze driehoek. Onderstaande commando’s tekenen dus een driehoek en een ovaal. ship_id = c.create_polygon(5, 5, 5, 25, 30, 15, fill='red') ship_id2 = c.create_oval(0, 0, 30, 30, outline='red') We houden in een constante ook bij hoe groot onze duikboot is. De cirkel heeft een straal van 15. Die waarde houden we bij in de constante SHIP_R. SHIP_R = 15 Nu gaan we onze duikboot in het midden van ons venster positioneren. Om het midden te bepalen delen we zowel de breedte als de hoogte door 2. Met de Canvas()-functie .move() verplaatsen we zowel de cirkel als de driehoek naar de nieuwe coördinaten. MID_X = BREEDTE / 2 MID_Y = HOOGTE / 2 c.move(ship_id, MID_X, MID_Y) c.move(ship_id2, MID_X, MID_Y) Onze basis-opzet is er: een blauwe zee met een rode duikboot in het midden. Nu moeten we zorgen dat we de duikboot kunnen besturen. Daarvoor maken we een constante waarin we de snelheid (speed) van het schip bijhouden: SHIP_SPD = 10 en vervolgens definiëren we een functie om het schip te bewegen. De functie zal als parameter een “event” meekrijgen. Die event zal vanuit ons hoofdprogramma komen en zal “Up”, “Down”, “Left” of “Right” zijn – afhankelijk van welke pijltjestoets we ingedrukt hebben. De functie werkt vrij simpel: bij elke pijltjesbeweging verplaatsen we ons schip met dezelfde .move()-functie die we daarnet al gebruikt hebben. Bij move() geven we drie parameters op: -
wat we willen bewegen (de driehoek of de cirkel) hoeveel we naar links en rechts willen bewegen – dit getal blijft 0 als we op en neer gaan, zal een negatief getal zijn als we naar links gaan en zal een positief getal zijn als we naar rechts gaan hoeveel we naar boven en onder willen bewegen – dit getal blijft 0 als we naar links en rechts gaan, zal een negatief getal zijn als we naar boven gaan en zal een positief getal zijn als we naar beneden gaan.
Python [P5] Project: Bellenschieter
versie 9/03/2015 3/11
De grootte van het getal wordt bepaald door onze snelheid (SHIP_SPD). def beweeg_schip(event): if event.keysym == 'Up': c.move(ship_id, 0, -SHIP_SPD) c.move(ship_id2, 0, -SHIP_SPD) elif event.keysym == 'Down': c.move(ship_id, 0, SHIP_SPD) c.move(ship_id2, 0, SHIP_SPD) elif event.keysym == 'Left': c.move(ship_id, -SHIP_SPD, 0) c.move(ship_id2, -SHIP_SPD, 0) elif event.keysym == 'Right': c.move(ship_id, SHIP_SPD, 0) c.move(ship_id2, SHIP_SPD, 0) De manier om deze functie te activeren bij het indrukken van een toets is via de functie .bind_all() die we op ons Canvas()-object kunnen toepassen. c.bind_all('
', beweeg_schip) We geven dus niet alleen de pijltjestoetsen door. Ook als je bv. spatiebalk drukt zal dit doorgegeven worden naar de functie beweeg_schip(). Maar we doen binnen die functie beweeg_schip() enkel iets met de pijltjestoetsen – de rest wordt gewoon genegeerd.
Ons spel is niet volledig zonder luchtbellen. Da’s dus de volgende stap. We gaan véél bellen hebben, dus we gaan de bellen bijhouden in een aantal lijsten. bub_id bub_r bub_speed
= list() = list() = list()
In bub_id houden we de identificatienummers van de bellen bij, in bub_r houden we de straal (de grootte) van de bellen bij en in bub_speed de snelheid waarmee ze bewegen. Er zijn veel verschillende bellen, van verschillende groottes. De grootte en de snelheid wordt willekeurig bepaald, maar we bepalen wel grenzen. We houden ook bij hoeveel afstand er tussen 2 bellen moet blijven: MIN_BUB_R MAX_BUB_R MAX_BUB_SPD TUSSEN
= = = =
10 30 10 100
Tijd voor een functie om de bellen effectief te maken. De bellen gaan van rechts naar links vliegen, dus we gaan ze laten “ontstaan” nét buiten het scherm, aan de rechterkant. De x-coördinaat – die bepaalt hoever iets naar rechts ligt wordt gelijk gesteld met de breedte van het venster + de tussenafstand tussen 2 bellen. De bel zal dus volledig buiten beeld zijn. Verder bepalen we een willekeurige hoogtepositie van de bel (y) en een willekeurige straal (r) , binnen de grenzen die we bepaald hadden. Python [P5] Project: Bellenschieter
versie 9/03/2015 4/11
def maak_bel(): x = BREEDTE + TUSSEN y = random.randint(0, HOOGTE) r = random.randint(MIN_BUB_R, MAX_BUB_R) id1 = c.create_oval(x - r, y - r, x + r, y + r, outline='white') bub_id.append(id1) bub_r.append(r) bub_speed.append(random.randint(1, MAX_BUB_SPD)) De bovenstaande functie sluit af met het effectieve tekenen van de bel (.create_oval()). De referentie naar de bel wordt eerst in een tijdelijke variabele id1 gestoken en vervolgens bij ons lijstje bub_id gehangen. Ook de straal hangen we bij de bub_r-lijst. In de bub_speed-lijst tenslotte steken we een willekeurig bepaalde snelheid voor de bel. In ons hoofdprogramma gaan we in een oneindige lus moeten de bellen laten bewegen. Daarvoor maken we een functie beweeg_bellen() waarin we telkens de hele lijst met bellen aflopen, en de positie van de bellen aanpassen via .move(). def beweeg_bellen(): for i in range(0, len(bub_id)): c.move(bub_id[i], -bub_speed[i], 0) We zullen ook een functie nodig hebben die de coördinaten kan opvragen van een opgegeven bel. Bekijk terug de tekening met de coördinaten op de x- en y-as enkele bladzijden terug. Om de coördinaten van het middelpunt van de cirkel te vinden moeten we de x- en y-coördinaten van de hoekpunten van de omliggende rechthoek optellen bij elkaar en delen door 2: def get_coords(id_num): pos = c.coords(id_num) x = (pos[0] + pos[2])/2 y = (pos[1] + pos[3])/2 return x, y We zijn er bijna met onze functies. We hebben nog een functie nodig om de bellen te doen verdwijnen. Bellen gaan verdwijnen zodra we ze aanraken met onze duikboot, of als we het programma opschonen (zie ietsje verder). Om een bel te laten verdwijnen, verwijderen we hem uit al onze lijstjes, en verwijderen we hem vanuit ons venster met het delete() commando. def verdwijn_bel(i): del bub_r[i] del bub_speed[i] c.delete(bub_id[i]) del bub_id[i] Om te zorgen dat ons programma niet vreselijk traag wordt, gaan we de bellen die niet meer in beeld zijn, ook laten “verdwijnen”. Dat doen we door bij elke stap te kijken of er bellen zijn waarvan de x-coördinaat negatief is. Die zijn dus buiten beeld.
Python [P5] Project: Bellenschieter
versie 9/03/2015 5/11
def bellen_opschonen(): for i in range(len(bub_id)-1, -1, -1): x, y = get_coords(bub_id[i]) if x < -TUSSEN: verdwijn_bel(i) We hebben nog een functie nodig om de afstand tussen 2 punten te berekenen. Hiervoor wordt de stelling van Pythagoras gebruikt. Je hoeft die niet te kennen en te snappen – gebruik gewoon onderstaande code: def afstand(id1, id2): x1, y1 = get_coords(id1) x2, y2 = get_coords(id2) return math.sqrt((x2 - x1)**2 + (y2 - y1)**2) En dan nog de lààtste bellen-functie die we nodig hebben: één om een bel te laten verdwijnen als we er met onze duikboot tegen zitten. Om dat te controleren gaan we de afstand tussen het middelpunt van elke bel en het middelpunt van onze duikboot berekenen. Als er ergens een bel is waarvan de afstand kleiner is dan de som van de straal van de bel en de straal van de duikboot, dan hebben we een botsing. In dat geval verdwijnt de bel, en krijgen we er punten voor.
def botsing(): punten = 0 for bub in range(len(bub_id)-1, -1, -1): if afstand(ship_id2, bub_id[bub]) < (SHIP_R + bub_r[bub]): punten += (bub_r[bub] + bub_speed[bub]) verdwijn_bel(bub) return punten Nu hebben we nog een functie nodig om de tijd en de score te tonen. Dat doen we met de Canvas()-functie .create_text(). # Score en tijd tonen c.create_text(50, 30, text='TIJD', fill='white') c.create_text(150, 30, text='SCORE', fill='white') time_text = c.create_text(50, 50, fill='white') score_text = c.create_text(150, 50, fill='white') def toon_score(score): c.itemconfig(score_text, text=str(score)) def toon_tijd(tijd_over): c.itemconfig(time_text, text=str(tijd_over))
Python [P5] Project: Bellenschieter
versie 9/03/2015 6/11
Daarmee hebben we alle functies. Tijd om het hoofdprogramma te laten lopen. Daarin bepalen we eerst nog een paar variabelen: -
BUB_CHANCE: bepaalt hoe vaak er een bel komt. Hoe hoger dit getal, hoe minder snel de bellen zullen komen. score: om onze score bij te houden TIJDSLIMIET: na hoeveel seconden het spel afgelopen is BONUS_SCORE: hoeveel we moeten halen om bonustijd te krijgen bonus: om onze bonusscore bij te houden eindtijd: om te bepalen wanneer het spel eindigt. Dat is de huidige tijd plus de tijdslimiet. # MAIN # BUB_CHANCE = 10 score = 0 TIJDSLIMIET = 30 BONUS_SCORE = 1000 bonus = 0 eindtijd = time.time() + TIJDSLIMIET
Nu laten we een herhalingslus lopen. Die blijft lopen zolang de huidige tijd kleiner is dan de bepaalde eindtijd. In de herhalingslus -
maken we een bel aan bewegen we alle bellen verwijderen we de bellen die niet meer in beeld zijn updaten we de score bekijken we of de bonusscore bereikt is updaten we de getoonde score en de getoonde tijd verversen we de inhoud van het venster wachten we 0,01 seconde voor we aan de volgende lus beginnen. while time.time() < eindtijd: if random.randint(1, BUB_CHANCE) == 1: maak_bel() beweeg_bellen() bellen_opschonen() score += botsing() if (int(score / BONUS_SCORE)) > bonus: bonus += 1 eindtijd += TIJDSLIMIET toon_score(score) toon_tijd(int(eindtijd - time.time())) venster.update() time.sleep(0.01)
Als we uit de lus komen, is het spel afgelopen. We voorzien nog wat tekst voor de “Game over”.
Python [P5] Project: Bellenschieter
versie 9/03/2015 7/11
# Game over c.create_text(MID_X, MID_Y, text='GAME OVER', fill='white', font=('Helvetica', 30)) c.create_text(MID_X, MID_Y+30, text='Score: ' + str(score), fill='white') c.create_text(MID_X, MID_Y+45, text='Bonustijd: ' + str(bonus*TIJDSLIMIET), fill='white')
Een voorbeeld van het programma Zo ziet het programma er uit in volle actie:
Python [P5] Project: Bellenschieter
versie 9/03/2015 8/11
De volledige code # Duikbootspel import tkinter import random import time import math HOOGTE = 500 BREEDTE = 800 # Maak het venster venster = tkinter.Tk() venster.title('Bellenschieter') c = tkinter.Canvas(venster, width=BREEDTE, height=HOOGTE, bg='darkblue') c.pack() # Maak de duikboot ship_id = c.create_polygon(5, 5, 5, 25, 30, 15, fill='red') ship_id2 = c.create_oval(0, 0, 30, 30, outline='red') SHIP_R = 15 MID_X = BREEDTE / 2 MID_Y = HOOGTE / 2 c.move(ship_id, MID_X, MID_Y) c.move(ship_id2, MID_X, MID_Y) # De duikboot besturen SHIP_SPD = 10 def beweeg_schip(event): if event.keysym == 'Up': c.move(ship_id, 0, -SHIP_SPD) c.move(ship_id2, 0, -SHIP_SPD) elif event.keysym == 'Down': c.move(ship_id, 0, SHIP_SPD) c.move(ship_id2, 0, SHIP_SPD) elif event.keysym == 'Left': c.move(ship_id, -SHIP_SPD, 0) c.move(ship_id2, -SHIP_SPD, 0) elif event.keysym == 'Right': c.move(ship_id, SHIP_SPD, 0) c.move(ship_id2, SHIP_SPD, 0) c.bind_all('', beweeg_schip) # Bellen definieren bub_id = list() bub_r = list() bub_speed = list() MIN_BUB_R = 10 MAX_BUB_R = 30 MAX_BUB_SPD = 10 TUSSEN = 100 def maak_bel(): x = BREEDTE + TUSSEN y = random.randint(0, HOOGTE) r = random.randint(MIN_BUB_R, MAX_BUB_R) id1 = c.create_oval(x - r, y - r, x + r, y + r, outline='white') bub_id.append(id1) bub_r.append(r) bub_speed.append(random.randint(1, MAX_BUB_SPD))
Python [P5] Project: Bellenschieter
versie 9/03/2015 9/11
# Laat de bellen bewegen def beweeg_bellen(): for i in range(0, len(bub_id)): c.move(bub_id[i], -bub_speed[i], 0) def get_coords(id_num): pos = c.coords(id_num) x = (pos[0] + pos[2])/2 y = (pos[1] + pos[3])/2 return x, y # Bellen doen verdwijnen def verdwijn_bel(i): del bub_r[i] del bub_speed[i] c.delete(bub_id[i]) del bub_id[i] def bellen_opschonen(): for i in range(len(bub_id)-1, -1, -1): x, y = get_coords(bub_id[i]) if x < -TUSSEN: verdwijn_bel(i) # Afstand tussen twee punten def afstand(id1, id2): x1, y1 = get_coords(id1) x2, y2 = get_coords(id2) return math.sqrt((x2 - x1)**2 + (y2 - y1)**2) # Laat de bellen knallen def botsing(): punten = 0 for bub in range(len(bub_id)-1, -1, -1): if afstand(ship_id2, bub_id[bub]) < (SHIP_R + bub_r[bub]): punten += (bub_r[bub] + bub_speed[bub]) verdwijn_bel(bub) return punten # Score en tijd tonen c.create_text(50, 30, text='TIJD', fill='white') c.create_text(150, 30, text='SCORE', fill='white') time_text = c.create_text(50, 50, fill='white') score_text = c.create_text(150, 50, fill='white') def toon_score(score): c.itemconfig(score_text, text=str(score)) def toon_tijd(tijd_over): c.itemconfig(time_text, text=str(tijd_over))
Python [P5] Project: Bellenschieter
versie 9/03/2015 10/11
# MAIN # BUB_CHANCE = 10 score = 0 TIJDSLIMIET = 30 BONUS_SCORE = 1000 bonus = 0 eindtijd = time.time() + TIJDSLIMIET while time.time() < eindtijd: if random.randint(1, BUB_CHANCE) == 1: maak_bel() beweeg_bellen() bellen_opschonen() score += botsing() if (int(score / BONUS_SCORE)) > bonus: bonus += 1 eindtijd += TIJDSLIMIET toon_score(score) toon_tijd(int(eindtijd - time.time())) venster.update() time.sleep(0.01) # Game over c.create_text(MID_X, MID_Y, text='GAME OVER', fill='white', font=('Helvetica', 30)) c.create_text(MID_X, MID_Y+30, text='Score: ' + str(score), fill='white') c.create_text(MID_X, MID_Y+45, text='Bonustijd: ' + str(bonus*TIJDSLIMIET), fill='white')
Python [P5] Project: Bellenschieter
versie 9/03/2015 11/11