.. role:: python(code)
:language: python
.. |br| raw:: html
Winnen en verliezen
====================
Bij veel games gaat het om winnen of verliezen. Als je wint, heb je een goede score en als je verliest is het 'game over'. In dit hoofdstuk maken we een spel waarin je aliens zo lang mogelijk in de lucht moet houden. Zodra een alien de grond raakt, is het spel afgelopen.
Het uitgangspunt voor het spel is de onderstaande code.
.. code-block:: python
:linenos:
:caption: alien.py
:name: alien_final_game_v00
# Vensterafmetingen
WIDTH = 600
HEIGHT = 400
# Roze alien Actor
alien = Actor('alien_pink')
alien.midbottom = (WIDTH / 2, 0)
alien.speed = 3
alien.jump_distance = 150
# De draw() functie van de game
def draw():
screen.clear()
alien.draw()
# De update() functie van de game
def update():
alien.y += alien.speed
if alien.bottom > HEIGHT:
alien.bottom = HEIGHT
# Mouse down event handler
def on_mouse_down(button, pos):
if alien.collidepoint(pos):
alien.y -= alien.jump_distance
Bij aanvang van de game bevindt de alien zich net boven de bovenrand van het venster. Hij valt met 3 pixels per frame naar beneden en wanneer de gebruiker met de muis op hem klikt, springt hij 150 pixels omhoog.
Game over
---------
Het spel is afgelopen zodra de alien de onderkant van het venster raakt. Het spel moet stoppen en de boodschap 'game over' verschijnt. Voor deze boodschap gebruiken we een sprite. Download de :download:`game_over sprite <../game_assets/alien/images/game_over.png>` naar je :file:`alien\\images` map.
.. image:: ../game_assets/alien/images/game_over.png
:scale: 50%
|br|
Voeg voor de 'game over' boodschap een nieuwe Actor :python:`game_over_message` toe, onder de :python:`alien` Actor:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 5
:emphasize-lines: 7-9
:caption: alien.py
:name: alien_final_game_v01
# Roze alien Actor
alien = Actor('alien_pink')
alien.midbottom = (WIDTH / 2, 0)
alien.speed = 3
alien.jump_distance = 150
# Game over message
game_over_message = Actor('game_over')
game_over_message.center = (WIDTH / 2, HEIGHT / 2)
Om de boodschap te tonen moeten we in de :python:`draw()` functie :python:`game_over_message.draw()` aanroepen. Wat gaat er mis als we dat doen op de onderstaande manier? Probeer het uit.
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 15
:emphasize-lines: 5
:caption: alien.py
# De draw() functie van de game
def draw():
screen.clear()
alien.draw()
game_over_message.draw()
Nu wordt 'game over' al vanaf het begin op het scherm getoond! Om ervoor te zorgen dat de sprite alleen zichtbaar is als het spel daadwerkelijk voorbij is, hebben we een variabele nodig:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 11
:emphasize-lines: 5-6
:caption: alien.py
# Game over message
game_over_message = Actor('game_over')
game_over_message.center = (WIDTH / 2, HEIGHT / 2)
# Variables
game_over = False
Je kunt in variabelen getallen opslaan maar ook de waarden :python:`True` en :python:`False` om aan te geven dat iets *waar* of *niet waar* is. De variabele :python:`game_over` stelt ons in staat om in de :python:`draw()` functie een :python:`if` statement te maken:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 18
:emphasize-lines: 5-6
:caption: alien.py
# De draw() functie van de game
def draw():
screen.clear()
alien.draw()
if game_over:
game_over_message.draw()
Het probleem is opgelost. De :python:`game_over_message` sprite is niet meer zichtbaar vanaf het begin. Echter, hij moet wél zichtbaar worden wanneer de alien de grond raakt. In welke functie detecteren we die gebeurtenis? Juist, in de :python:`update()` functie. In het :python:`if` statement dat het raken van de grond afhandelt, zetten we :python:`game_over` op :python:`True`:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 25
:emphasize-lines: 6
:caption: alien.py
# De update() functie van de game
def update():
alien.y += alien.speed
if alien.bottom > HEIGHT:
alien.bottom = HEIGHT
game_over = True
Nu zou het moeten werken, toch? Probeer maar eens.
De 'game over' boodschap verschijnt niet. Er gebeurt helemaal niets. De oorzaak hiervan is een beetje ingewikkeld. Hou je wel van ingewikkelde dingen, klap dan de onderstaande uitleg over globale variabelen open.
.. _globale-variabelen:
.. dropdown:: Meer weten over globale variabelen?
:color: info
:icon: info
In Python creëer je een variabele met een assignment statement:
.. card:: Assignment statement
:python:` = `
Het maakt echter uit op welke plek dat assignment statement staat. Bekijk de volgende code. Als je het zelf wilt testen, maak dan een nieuw bestand in Mu editor en stel de mode van Mu editor in op Python 3.
.. code-block:: python
:linenos:
:emphasize-lines: 1, 5
g = 1 # Globale variabele
# Functie spam()
def spam():
l = 2 # Lokale variabele
print(l)
# Hoofdprogramma
print(g)
spam()
| Op regel 1 wordt de variabele :python:`g` gemaakt. Dit is een globale variabele, **want deze regel bevindt zich niet in een functie**.
| Op regel 5 wordt de variabele :python:`l` gemaakt. Dit is een lokale variabele, **want deze regel bevindt zich wél in een functie**.
Globale variabelen zijn in je hele programma bekend, ook in de functies. Lokale variabelen echter zijn enkel bekend binnen de functie waarin ze zijn gemaakt. Buiten die functie bestaan ze niet.
Het volgende kan dus:
.. code-block:: python
:linenos:
:emphasize-lines: 7
g = 1 # Globale variabele
# Functie spam()
def spam():
l = 2 # Lokale variabele
print(l)
print(g) # Globale variabele g is hier bekend
# Hoofdprogramma
print(g)
spam()
Maar het volgende levert een foutmelding op:
.. code-block:: python
:linenos:
:emphasize-lines: 11
g = 1 # Globale variabele
# Functie spam()
def spam():
l = 2 # Lokale variabele
print(l)
# Hoofdprogramma
print(g)
spam()
print(l) # Lokale variabele l is hier niet bekend
De oplettende lezer zal nu zeggen: 'Dan is er toch geen probleem? Onze :python:`game_over` variabele in :file:`alien.py` is globaal, dus bekend in de :python:`update()` functie.' Dat is waar, maar er zit nog een addertje (een Python?) onder het gras: van globale variabelen kun je binnen een functie wel de waarde opvragen, maar je kunt er niet een nieuwe waarde in opslaan. Ons assignment statement in regel 31 wordt door Python geïnterpreteerd als het aanmaken van een nieuwe variabele :python:`game_over`. We hebben dan dus twee variabelen met dezelfde naam, een globale en een lokale.
.. code-block:: python
# Variables
game_over = False # Globale variabele game_over
...
# De update() functie van de game
def update():
...
game_over = True # Lokale variabele game_over
Om een globale variabele binnen een functie te kunnen wijzigen, moet je bovenaan die functie het keyword :python:`global` gebruiken, gevolgd door de naam van de variabele. Dus:
.. code-block:: python
:emphasize-lines: 8
# Variables
game_over = False # Globale variabele game_over
...
# De update() functie van de game
def update():
global game_over
...
game_over = True # Globale variabele game_over
.. dropdown:: Waarschuwing
:open:
:color: warning
:icon: alert
Het gebruik van globale variabelen is eigenlijk 'bad practice'; je kunt ze beter zo min mogelijk gebruiken, zeker wanneer je grotere programmeerprojecten gaat doen. Voor ons kleine alien spelletje is het echter geen probleem.
Als je even geen trek hebt in ingewikkelde dingen, maar wel graag je game werkend wil maken, voeg dan bovenaan de :python:`update()` functie de volgende regel toe:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 25
:emphasize-lines: 3
:caption: alien.py
# De update() functie van de game
def update():
global game_over
alien.y += alien.speed
if alien.bottom > HEIGHT:
alien.bottom = HEIGHT
game_over = True
Misschien heb je het al gemerkt: na het verschijnen van de 'game over' boodschap kun je nog steeds op de alien klikken om hem te laten springen. Om dat op te lossen, voegen we een extra if-statement toe aan de :python:`on_mouse_down()` event handler:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 33
:emphasize-lines: 3-4
:caption: alien.py
# Mouse down event handler
def on_mouse_down(button, pos):
if game_over:
return
if alien.collidepoint(pos):
alien.y -= alien.jump_distance
Het keyword :python:`return` zorgt ervoor dat Python direct terugkeert uit de functie, zonder de rest van de code uit te voeren.
Score
------
In ons alien spel is je score eigenlijk de tijd; hoe langer je de alien in de lucht houdt, hoe hoger je score. Daarvoor hebben we de Pygame Zero :python:`clock` nodig. Maar laten we beginnen met het maken van een :python:`score` variabele:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 15
:emphasize-lines: 3
:caption: alien.py
# Variables
game_over = False
score = 0
Om de score elke seconde met een punt te verhogen, moeten we een nieuwe functie maken. Definieer de functie :python:`increment_score()` als volgt:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 15
:emphasize-lines: 5-8
:caption: alien.py
# Variables
game_over = False
score = 0
# Functie increment_score() verhoogt de score
def increment_score():
global score
score += 1
Ook hier moet je weer het keyword :python:`global` gebruiken om de waarde van de globale variabele :python:`score` te kunnen wijzigen binnen de functie.
Het tonen van de score gebeurt in de :python:`draw()` functie:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 24
:emphasize-lines: 5
:caption: alien.py
# De draw() functie van de game
def draw():
screen.clear()
alien.draw()
screen.draw.text(f"Score: {score}", (10, 10), color = "yellow", fontsize = 40)
if game_over:
game_over_message.draw()
We zijn er bijna. We moeten er alleen nog voor zorgen dat de functie :python:`increment_score` elke seconde wordt aangeroepen. Daarvoor gebruiken we de functie :python:`clock.schedule_interval()`. Deze hoeft slechts één keer te worden aangeroepen, helemaal aan het begin van de game. Daarom plaatsen we de aanroep helemaal onderaan in het 'hoofdprogramma':
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 40
:emphasize-lines: 8-9
:caption: alien.py
# Mouse down event handler
def on_mouse_down(button, pos):
if game_over:
return
if alien.collidepoint(pos):
alien.y -= alien.jump_distance
# Hoofdprogramma
clock.schedule_interval(increment_score, 1)
Tussen de haakjes van :python:`clock.schedule_interval()` staat eerst de naam van de functie die we telkens willen aanroepen gevolgd door het tijdsinterval in seconden. Regel 48 zorgt er dus voor dat :python:`increment_score` elke :python:`1` seconde wordt aangeroepen.
Onze game is nu al redelijk speelbaar, maar er gaat nog iets niet helemaal goed. Kun je ontdekken wat dat is?
Je kunt het probleem op de volgende manier oplossen:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 32
:emphasize-lines: 8
:caption: alien.py
# De update() functie van de game
def update():
global game_over
alien.y += alien.speed
if alien.bottom > HEIGHT:
alien.bottom = HEIGHT
game_over = True
clock.unschedule(increment_score)
Na het verschijnen van 'game over', werd de score nog steeds elke seconde opgehoogd. Door het aanroepen van :python:`clock.unschedule(increment_score)` is dat gestopt.
Achtergrond(muziek)
-------------------
Ons spel kan nog wel wat verfraaiing gebruiken. We gaan een achtergrondafbeelding toevoegen en ook een achtergrondmuziekje. Download de :download:`achtergrondafbeelding <../game_assets/alien/images/background.jpg>` naar je :file:`alien\\images` map.
.. image:: ../game_assets/alien/images/background.jpg
:scale: 50%
|br|
Om de achtergrondafbeelding zichtbaar te maken, vervang je de aanroep :python:`screen.clear()` in regel 26 door :python:`screen.blit('background', (0, 0))`.
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 24
:emphasize-lines: 3
:caption: alien.py
# De draw() functie van de game
def draw():
screen.blit('background', (0, 0))
alien.draw()
screen.draw.text(f"Score: {score}", (10, 10), color = "yellow", fontsize = 40)
if game_over:
game_over_message.draw()
Bekijk het resultaat. Dat ziet er meteen een stuk beter uit toch? Omdat een achtergrond statisch is (niet hoeft te bewegen), hebben we er geen :python:`Actor` voor aangemaakt, zoals we dat met de alien en de 'game over' boodschap deden. De aanroep :python:`screen.clear()` is overbodig geworden omdat de achtergrond telkens opnieuw wordt getekend wanneer de alien zich verplaatst.
Klik in Mu editor op de :guilabel:`Music` knop om de map :file:`alien\\music` aan te maken en te openen. Download vervolgens de :download:`achtergrondmuziek <../game_assets/alien/music/astro_race.mp3>` naar die map. Overigens is deze muziek afkomstig van `Zapsplat `_, een website waar je muziek zonder copyrights kunt downloaden. Ook vind je hier een grote verzameling geluidseffecten voor je games.
Om de muziek te laten afspelen hoef je slechts één regel aan het hoofdprogramma toe te voegen:
.. code-block:: python
:class: no-copybutton
:linenos:
:lineno-start: 48
:emphasize-lines: 3
:caption: alien.py
# Hoofdprogramma
clock.schedule_interval(increment_score, 1)
music.play('astro_race')
De basis van het spel is nu klaar. Ben je ergens halverwege de draad kwijtgeraakt, dan kun je hieronder de volledige code bekijken. Natuurlijk zijn nog allerhande verbeteringen mogelijk. In het volgende hoofdstuk bekijken we enkele van die verbetermogelijkheden.
.. figure:: images/game_final.png
.. dropdown:: Volledige code van het spel
:color: info
:icon: info
.. code-block:: python
:linenos:
:caption: alien.py
# Vensterafmetingen
WIDTH = 600
HEIGHT = 400
# Roze alien Actor
alien = Actor('alien_pink')
alien.midbottom = (WIDTH / 2, 0)
alien.speed = 3
alien.jump_distance = 150
# Game over message
game_over_message = Actor('game_over')
game_over_message.center = (WIDTH / 2, HEIGHT / 2)
# Variables
game_over = False
score = 0
# Functie increment_score() verhoogt de score
def increment_score():
global score
score += 1
# De draw() functie van de game
def draw():
screen.blit('background', (0, 0))
alien.draw()
screen.draw.text(f"Score: {score}", (10, 10), color = "yellow", fontsize = 40)
if game_over:
game_over_message.draw()
# De update() functie van de game
def update():
global game_over
alien.y += alien.speed
if alien.bottom > HEIGHT:
alien.bottom = HEIGHT
game_over = True
clock.unschedule(increment_score)
# Mouse down event handler
def on_mouse_down(button, pos):
if game_over:
return
if alien.collidepoint(pos):
alien.y -= alien.jump_distance
# Hoofdprogramma
clock.schedule_interval(increment_score, 1)
music.play('astro_race')