1. Explainer: sprites draaien#

Voor sommige spellen is het belangrijk dat sprites kunnen draaien. In deze explainer leer je hoe je sprites kunt draaien in Pygame Zero. En we gaan nog een stapje verder: hoe kun je een kanon richten op een doelwit en de kanonskogels in de juiste richting afvuren?

../_images/cannon_animation_small.gif

Voorbereidingen#

Maak voor dit project in je games map een nieuwe map aan met de naam cannon. Maak in die map ook een map images aan. Download de sprites van het kanon en de kanonskogel: cannon.png en cannonball.png en plaats ze in de images map.

Maak in Mu Editor een nieuw bestand en sla het op in je cannon map onder de naam cannon.py.

@startuml
   @startfiles
   /games/cannon/images/cannon.png
   /games/cannon/images/cannonball.png
   /games/cannon/cannon.py
   @endfiles
@enduml

Starter code#

Hieronder staat de starter code voor het project. Kopier de code naar je cannon.py bestand.

cannon.py#
 1# WINDOW SETTINGS
 2
 3WIDTH = 600
 4HEIGHT = 400
 5TITLE = 'Cannon aiming'
 6
 7# SPRITES
 8
 9cannon = Actor('cannon')
10cannon.anchor = (40, 46)
11cannon.x = WIDTH / 2
12cannon.y = HEIGHT - 40
13
14# DRAW AND UPDATE FUNCTIONS
15
16def draw():
17   screen.fill('darkgreen')
18   cannon.draw()
19
20def update():
21   pass

In regel 10 staat cannon.anchor = (40, 46). Daarmee stellen we het anchor point van het kanon in. De ankerpositie is het punt waaromheen het kanon later gaat draaien. De coördinaten (40, 46) zijn het middelpunt van de cirkel op het kanon:

../_images/cannon_anchor.png

Doordat we een anchor point hebben ingesteld, verwijzen cannon.x en cannon.y nu ook naar het ankerpunt van het kanon. In regels 11 en 12 positioneren we het kanon dus zodanig dat het anchor point horizontaal in het midden en verticaal op 40 pixels van de onderrand komt te liggen. Meer informatie over anchor points vind je in de Pygame Zero documentatie.

../_images/cannon_01.png

Het angle attribuut#

Het kanon is nu naar rechts gericht, maar het is mooier om het bij aanvang van het spel recht omhoog te laten wijzen. We kunnen het kanon draaien door de cannon.angle eigenschap in te stellen:

 7# SPRITES
 8
 9cannon = Actor('cannon')
10cannon.anchor = (40, 46)
11cannon.x = WIDTH / 2
12cannon.y = HEIGHT - 40
13cannon.angle = 90

Met regel 13 draaien we het kanon 90 graden tegen de klok in, zodat het recht omhoog wijst.

../_images/cannon_02.png

Plaats bij wijze van experiment eens de volgende regel in de update() functie:

21def update():
22   cannon.angle += 3

Run de code en je ziet dat het kanon rondjes draait. Om het belang van het juiste anchor point te demonstreren, kun je in regel 10 het anchor point een andere waarde geven, bijvoorbeeld (0, 0):

10cannon.anchor = (0, 0)

Met deze instelling draait het kanon rond de linkerbovenhoek van de sprite.
Zet nadat je klaar bent met experimenteren het anchor point weer terug naar de oorspronkelijke waarde (40, 46) en vervang de regel in de update() functie weer door het pass keyword.

In de Pygame Zero documentatie staat een voorbeeld van een sprite die draait naar de muispositie door gebruik te maken van de angle_to() methode. Dat voorbeeld kunnen we één-op-één gebruiken om het kanon naar de muispositie te laten draaien:

15# EVENT HANDLERS
16
17def on_mouse_move(pos):
18   cannon.angle = cannon.angle_to(pos)

Run de code en beweeg de muis over het scherm. Het kanon draait nu naar de muispositie.

Vectoren#

Zoals je ziet, is het draaien van een sprite heel eenvoudig. Het afschieten van kanonskogels is echter iets ingewikkelder. We willen namelijk dat de kanonskogel wordt afgeschoten in de richting die overeenkomt met het kanon. Hoe bepaal je die richting? En hoe laat je de sprite van de kanonskogel vervolgens in die richting bewegen? Om dat voor elkaar te krijgen, moet je eerst begrijpen wat een vector is.

Een vector is een wiskundig begrip dat meestal wordt weergegeven als een pijl. Deze pijl heeft een richting en een lengte. Hieronder zie je drie voorbeelden van vectoren.

../_images/vectors_01.png

In de wiskunde worden bovenstaande vectoren als volgt genoteerd:

\[\begin{split}\textcolor{red}{\vec{a} = \begin{pmatrix} 3 \\ 2 \end{pmatrix}}, \quad \textcolor{blue}{\vec{b} = \begin{pmatrix} -1 \\ 3 \end{pmatrix}}, \quad \textcolor{green}{\vec{c} = \begin{pmatrix} 0 \\ -2 \end{pmatrix}}\end{split}\]

Kijk eens goed naar de getallen tussen de haakjes en naar de pijlen. Zie wat de getallen betekenen? De eerste waarde is de horizontale component van de vector en de tweede waarde is de verticale component. Bijvoorbeeld vector \(\vec{a}\) heeft een horizontale component van 3 en een verticale component van 2. Dat betekent dat de pijl 3 hokjes naar rechts wijst en 2 hokjes omhoog.

We kunnen vectoren ook op een andere manier noteren, namelijk door de lengte en de hoek van de vector te gebruiken. De lengte van een vector is de afstand tussen het begin- en eindpunt van de pijl. De hoek is de richting waarin de pijl wijst, gemeten vanaf de positieve x-as (de horizontale as).

../_images/vectors_02.png
\[\textcolor{red}{\vec{a} = \begin{pmatrix} 3.6, 33.7^{\circ} \end{pmatrix}}, \quad \textcolor{blue}{\vec{b} = \begin{pmatrix} 3.2, 108.4^{\circ} \end{pmatrix}}, \quad \textcolor{green}{\vec{c} = \begin{pmatrix} 2, 270^{\circ} \end{pmatrix}}\]

Blijkbaar heeft vector \(\vec{a}\) een lengte van 3.6 en een hoek van 33.7°. Hoe je deze waarden kunt berekenen, is een onderwerp voor een andere keer. Voor nu is het voldoende om te begrijpen dat het mogelijk is om de x- en y-componenten van een vector om te rekenen naar de lengte en de hoek van de vector, en vice versa.

De Vector2 class#

In de module pygame.math bevindt zich de class Vector2, waarmee je 2-dimensionale vectoren kunt maken en bewerken. Deze class heeft een aantal handige methoden die het ingewikkelde rekenwerk voor je doen. Meer informatie vind je in de Pygame documentatie. We gaan de Vector2 class gebruiken om de richting van de kanonskogels te bepalen.

Wat is een class?

De term class komt uit het objectgeoriënteerd programmeren. Objectgeoriënteerd programmeren is een techniek waarbij je je code organiseert met behulp van objecten. Die objecten hebben hun eigen variabelen (properties) en functies (methods). Een class is een soort sjabloon of blauwdruk voor het maken van objecten.

In Pygame is Actor een voorbeeld van een class. De Actor class heeft properties zoals image, x, y en methoden zoals draw(). De Actor class is een soort blauwdruk voor het maken van acteurs (sprites) in Pygame.

Wil je meer weten over classes en objecten? Kijk dan eens hier.

Om de Vector2 class te gebruiken, moeten we deze eerst importeren:

1from pygame.math import Vector2

Vervolgens maken we een lege lijst aan waarin we de kanonskogels gaan opslaan, en we definiëren een constante voor de snelheid van de kanonskogels:

 9# SPRITES
10
11cannon = Actor('cannon')
12cannon.anchor = (40, 46)
13cannon.x = WIDTH / 2
14cannon.y = HEIGHT - 40
15cannon.angle = 90
16
17cannonballs = []
18SPEED = 6

De helper functie spawn_cannonball() maakt een nieuwe kanonskogel aan en voegt deze toe aan de lijst van kanonskogels. De spawn positie en de richting zijn parameters van deze functie:

20# HELPER FUNCTIONS
21
22def spawn_cannonball(pos, velocity):
23   cannonball = Actor('cannonball')
24   cannonball.center = pos
25   cannonball.velocity = velocity
26   cannonballs.append(cannonball)

En nu komt het moeilijkste deel. We maken een helper functie fire_cannon() die de kanonskogel afvuurt. Deze functie berekent de richting van de kanonskogel op basis van de hoek van het kanon. We gebruiken hiervoor de Vector2 class:

28def fire_cannon():
29   direction = Vector2(1, 0).rotate(-cannon.angle)
30   direction.scale_to_length(100)
31   spawn_pos = Vector2(cannon.x, cannon.y) + direction
32   direction.scale_to_length(SPEED)
33   spawn_cannonball(spawn_pos, direction)

In regel 29 wordt een nieuwe vector gemaakt die naar rechts wijst (1, 0). Deze vector wordt vervolgens gedraaid met de negatieve waarde van de hoek van het kanon. Waarom de negatieve waarde? Omdat in Pygame de y-as omgedraaid is (zie ook hier). Door deze rotatie wijst de vector in de richting waarin het kanon gericht is.
In regel 30 wordt de lengte van de vector aangepast naar 100 pixels. Dit is de afstand tussen het ankerpunt van het kanon en de spawn positie van de kanonskogel.

../_images/cannon_vectors.png

In regel 31 berekenen we de spawn positie van de kanonskogel door de direction vector op te tellen bij de ankerpositie van het kanon. De spawn positie is dus het ankerpunt van het kanon plus een afstand van 100 pixels in de richting van de kanonskogel.
In regel 32 passen we de lengte van de vector opnieuw aan, maar nu naar de waarde van SPEED. Daarmee wordt de lengte van de vector de snelheid waarmee de kanonskogel zich straks gaat verplaatsen.
In regel 33 roepen we spawn_cannonball() aan om de kanonskogel te maken en toe te voegen aan de lijst van kanonskogels.

Om de kanonskogel af te vuren, moeten we de fire_cannon() functie aanroepen. We doen dit in de on_mouse_down() event handler:

35# EVENT HANDLERS
36
37def on_mouse_move(pos):
38   cannon.angle = cannon.angle_to(pos)
39
40def on_mouse_down(pos, button):
41   fire_cannon()

Als je nu de code runt, zie je geen kanonskogels verschijnen. Dat komt doordat we de kanonskogels nog niet tekenen. We doen dit in de draw() functie:

45def draw():
46   screen.fill('darkgreen')
47   cannon.draw()
48   for cannonball in cannonballs:
49      cannonball.draw()

Als het goed is, zie je nu de kanonskogels verschijnen wanneer je in het venster klikt. We hebben de beweging nog niet geprogrammeerd, dus de kanonskogels blijven op hun plaats staan, maar je kunt wel zien dat ze op de goede plek verschijnen.

../_images/cannon_03.png

Doordat de velocity van de kanonskogel een vector is, kunnen we de kanonskogel eenvoudig laten bewegen door in de update() functie de x en y coördinaten van de kanonskogel bij te werken met de velocity vector:

51def update():
52   for cannonball in cannonballs:
53      cannonball.x += cannonball.velocity.x
54      cannonball.y += cannonball.velocity.y

Dat is alles! Nu schieten de kanonskogels alle richtingen op, afhankelijk van de hoek van het kanon.

Gravity#

In het echt vliegen kanonskogels niet in een rechte lijn, maar vallen ze naar beneden door de zwaartekracht. We kunnen dit simuleren met slechts twee regels code. We voegen eerst een constante GRAVITY toe:

17cannonballs = []
18SPEED = 6
19GRAVITY = 0.1

En vervolgens verhogen we in de update() functie de y-component van de velocity vector van de kanonskogel met de zwaartekracht:

52def update():
53   for cannonball in cannonballs:
54      cannonball.x += cannonball.velocity.x
55      cannonball.y += cannonball.velocity.y
56      cannonball.velocity.y += GRAVITY

Memory leaks voorkomen#

Wanneer je een kanonskogel afvuurt, wordt deze toegevoegd aan de lijst van kanonskogels. Maar als de kanonskogel het scherm verlaat, blijft deze in de lijst staan. Dit kan leiden tot een memory leak, waarbij het geheugen volloopt met onnodige objecten. Om dit te voorkomen, moeten we de kanonskogels verwijderen die het scherm verlaten. We doen dit door in de update() functie te controleren of de kanonskogel buiten het scherm is. Als dat het geval is, verwijderen we de kanonskogel uit de lijst:

52def update():
53   for cannonball in cannonballs.copy():
54      if cannonball.top > HEIGHT:
55            cannonballs.remove(cannonball)
56      else:
57            cannonball.x += cannonball.velocity.x
58            cannonball.y += cannonball.velocity.y
59            cannonball.velocity.y += GRAVITY

Waarom we in regel 53 een kopie van de cannonballs lijst gebruiken kun je hier teruglezen.
In regel 54 controleren we of de bovenkant van de kanonskogel onder de onderkant van het venster ligt. Omdat we gravity gebruiken, is dat voldoende; alle kanonskogels komen op een bepaald moment onder de vensterrand. Maar stel dat je laserstralen afvuurt die niet vallen, dan zou je ook moeten controleren of de straal links, rechts of boven buiten het venster is.

Tenslotte#

Uiteraard kun je deze code zelf verder aanpassen en uitbreiden. In plaats van de muis zou je het toetsenbord kunnen gebruiken om het kanon te draaien en de kanonskogels af te vuren. Je zou ook de snelheid van de kanonskogels kunnen aanpassen op basis van de positie van de muis of van hoe lang een toets is ingedrukt. En natuurlijk zou je een doelwit kunnen toevoegen dat moet worden geraakt.