7. Elementen tekenen#
In de grafische versie van het spel worden de elementen weergegeven als rechthoekige blokjes waarin links de afbeelding van het element staat en rechts de naam.
Voor zo’n blokje - laten we het een element card noemen - kunnen we geen Actor gebruiken, omdat het uit meer dan een plaatje bestaat. We zullen de element cards dus zelf moeten tekenen en dat is niet eenvoudig. Ten eerste moeten we de afbeelding van het element verkleinen naar de juiste grootte. De afbeeldingen in de images map zijn namelijk 512 x 512 pixels groot, terwijl voor ons 32 x 32 pixels meer dan genoeg is. We zouden natuurlijk alle afbeeldingen in een grafisch programma zoals Paint.NET kunnen verkleinen, maar dat is veel werk, en daarom gaan we het in Python doen. Een tweede, nog uitdagender vraagstuk is het passend maken van de rechthoek. De breedte van de rechthoek is namelijk afhankelijk van de tekst die we erin willen zetten. We moeten dus eerst de tekst meten voordat we de rechthoek kunnen tekenen. Gelukkig kunnen we dat ook in Python doen, maar het vergt wel wat rekenwerk en een paar extra functies.
Uitbreiding van de elements dictionary#
Tot nu toe staan in de elements dictionary alleen de namen van de elementen. Om straks het juiste plaatje bij het juiste element te kunnen tekenen, breiden we de dictionary uit met de namen van de afbeeldingen. Vervang het bestand elements.json in je alchemy map dit nieuwe bestand: elements.json.
Als je dit bestand opent, zie je dat de values van de dictionary niet meer eenvoudige strings zijn, maar subdictionaries met daarin het label (de naam die wordt getoond) van het element en de naam van de afbeelding.
Omdat de Pygame versie van het spel een andere structuur heeft dan de tekstversie, beginnen we weer helemaal ‘from scratch’ met de code. Maak in Mu Editor een nieuw bestand aan en typ de onderstaande code erin. Sla het bestand op in je alchemy map onder de naam alchemy.py.
1################
2# ALCHEMY GAME #
3# Pygame Zero #
4################
5
6import json
7
8# WINDOW SETTINGS
9
10WIDTH = 800
11HEIGHT = 450
12TITLE = 'Alchemy'
13
14# DICTIONARIES AND LISTS
15
16elements = {}
17recipes = {}
18
19# HELPER FUNCTIONS
20
21def load_elements():
22 global elements
23 with open('elements.json', 'r') as file:
24 elements = json.load(file)
25
26# DRAW FUNCTIONS
27
28def draw():
29 pass
30
31# UPDATE FUNCTION
32
33def update():
34 pass
35
36# MAIN PROGRAM
37
38load_elements()
39print(elements)
Probeer te voorspellen wat deze code doet voordat je hem uitvoert. De inhoud van de functie load_elements() zou je bekend moeten voorkomen van de tekstversie. Wat denk je dat er in de console verschijnt als je de code runt? Voer de code uit en kijk of je gelijk had. Verwijder vervolgens regel 40, want die was alleen om te testen of de dictionary goed werd geladen.
Afbeeldingen verkleinen#
Pygame Zero heeft geen voor ons geschikte functie om afbeeldingen te verkleinen, maar Pygame (zonder de Zero) heeft die wél. Voeg op regel 6 de pygame module toe aan het import statement:
6import json, pygame
Voor de element cards gaan we een aantal constanten gebruiken om de afmetingen te bepalen. De eerste is ICONSIZE. Deze constante krijgt de waarde 32 omdat we de icoontjes op de element cards 32 x 32 pixels willen maken. Voeg de volgende code in tussen de # WINDOW SETTINGS en de # DICTIONARIES AND LISTS secties:
12TITLE = 'Alchemy'
13
14# CARD SETTINGS
15
16ICONSIZE = 32
17
18# DICTIONARIES AND LISTS
Voor het daadwerkelijk tekenen van een element card definiëren we een nieuwe functie draw_element_card(). Deze functie heeft de volgende parameters nodig:
element_id: de naam van het element dat we willen tekenen. Dit is de key waaronder het element in de dictionary vindbaar is, zoals'water'of'fire'.pos: de positie waar we de element card willen tekenen. Dit is een tuple met de x- en y-coördinaten.
Voeg de volgende code toe aan de # DRAW FUNCTIONS sectie:
30# DRAW FUNCTIONS
31
32def draw_element_card(element_id, pos):
33 element = elements[element_id]
34 lbl = element['label']
35 img_name = element['image']
36 img = pygame.transform.scale(eval(f'images.{img_name}'), (ICONSIZE, ICONSIZE))
37 screen.blit(img, pos)
38
39def draw():
40 draw_element_card('earth', (20,20))
41
42# UPDATE FUNCTION
In regel 15 worden de gegevens van het element opgezocht in de dictionary. Vervolgens wordt op regel 16 het label van het element opgehaald en op regel 17 de naam van de bijbehorende afbeelding.
In regel 18 gebeuren meerdere dingen tegelijk. De expressie eval(f'images.{img_name}') is nodig omdat Pygame Zero alle afbeeldingen in de images map in een object met de naam images plaatst. Om bijvoorbeeld de afbeelding van aarde op te halen, moeten we images.earth gebruiken. Maar onze img_name is een string met aanhalingstekens ('earth' ) en geen variabele (earth). De eval() functie lost dit probleem op. Het maakt van de string f'images.{img_name}' Python code. Klinkt ingewikkeld en dat is het ook. Online kun je meer informatie vinden over de eval() functie (bijvoorbeeld hier), maar voor nu is het voldoende om te weten dat het werkt.
Ook verkleinen we in regel 18 de afbeelding naar de juiste grootte met de pygame.transform.scale() functie. Deze functie heeft twee argumenten nodig: de afbeelding die we willen verkleinen en een tuple met de nieuwe breedte en hoogte. De nieuwe breedte en hoogte zijn beide gelijk aan de constante ICONSIZE. De verkleinde afbeelding wordt opgeslagen in de variabele img. In regel 19 tekenen we de afbeelding op het scherm met de screen.blit() functie, die je al eerder bent tegengekomen voor het tekenen van achtergronden.
In regel 22 roepen we in de draw() functie de draw_element_card() functie aan om de element card van aarde te tekenen op positie (20, 20).
Run de code en kijk of de afbeelding van het element aarde verschijnt. Uiteraard kun je ook de afbeelding van een ander element tekenen door de naam van het element in regel 22 te veranderen. Probeer dat eens uit.
Afmetingen van de rechthoek#
Om te berekenen hoe groot de rechthoek van de element card moet zijn, definiëren we eerst een aantal constanten voor de marges: de afstanden tussen de inhoud van de rechthoek en de rand van de rechthoek. In onderstaande figuur zie je welke afstanden een rol spelen.
In de code noemen we deze afstanden LEFTMARGIN, RIGHTMARGIN, TOPMARGIN en BOTTOMMARGIN. Voor de horizontale ruimte tussen de afbeelding en de tekst maken we de constante HSPACE. Voeg deze constanten toe aan de # CARD SETTINGS sectie:
14# CARD SETTINGS
15
16ICONSIZE = 32
17LEFTMARGIN = 0
18RIGHTMARGIN = 10
19TOPMARGIN = 3
20BOTTOMMARGIN = 3
21HSPACE = 5
Om te berekenen hoe breed de rechthoek moet worden, hebben we informatie nodig over de breedte van de tekst. Die is afhankelijk van het lettertype en de lettergrootte en de tekst. Ook deze gegevens horen bij de # CARD SETTINGS. Voeg het volgende toe:
14# CARD SETTINGS
15
16ICONSIZE = 32
17LEFTMARGIN = 0
18RIGHTMARGIN = 10
19TOPMARGIN = 3
20BOTTOMMARGIN = 3
21HSPACE = 5
22FONTSIZE = 30
23card_font = pygame.font.SysFont(None, FONTSIZE)
24CARD_HEIGHT = TOPMARGIN + max(ICONSIZE, FONTSIZE) + BOTTOMMARGIN
In regel 22 kiezen we lettergrootte 30. Dat betekent dat het lettertype 30 pixels hoog is.
In regel 23 maken we een lettertype object aan met de pygame.font.SysFont() functie (waarover je hier meer kunt vinden). Deze functie heeft twee argumenten nodig: het lettertype dat we willen gebruiken (in dit geval None, wat betekent dat we het standaard lettertype gebruiken) en de lettergrootte. Het resultaat wordt opgeslagen in de constante card_font.
In regel 24 berekenen we de hoogte van de rechthoek. Deze is gelijk aan de grootste waarde van de constante ICONSIZE en de constante FONTSIZE, plus de marges aan de boven- en onderkant. De hoogte van de rechthoek is dus altijd minimaal even hoog als het plaatje of de tekst, plus de marges.
En nu begint het echte werk. We gaan een helper functie maken die:
alle items in de
elementsdictionary langsloopt;van elk element de breedte van de tekst berekent;
de breedte van de rechthoek berekent;
de afmetingen van de rechthoek opslaat in de
elementsdictionary onder een nieuwe key.
In code ziet dat er zo uit:
38def calc_card_rects():
39 for key, value in elements.items():
40 lbl_width, lbl_height = card_font.size(value['label'])
41 rect_width = LEFTMARGIN + ICONSIZE + HSPACE + lbl_width + RIGHTMARGIN
42 rect_height = CARD_HEIGHT
43 r = Rect((0, 0), (rect_width, rect_height))
44 elements[key]['rect'] = r
De code for key, value in elements.items() in regel 40 is een handige manier om alle items in een dictionary langs te lopen. De regels 41 t/m 45 worden dus uitgevoerd voor elk item in de elements dictionary.
In regel 41 gebruiken we de card_font.size() methode om de breedte en hoogte van de labeltekst te berekenen. De hoogte hebben we niet nodig, maar de size() methode geeft altijd zowel de breedte als de hoogte terug, dus we moeten beide waarden opslaan in variabelen (meer info hier).
Vervolgens berekenen we in regels 42 en 43 de breedte en hoogte van de rechthoek. De breedte is gelijk aan de som van de linker marge, de breedte van het plaatje, de horizontale ruimte tussen het plaatje en de tekst, de breedte van de tekst en de rechter marge.
In regel 44 maken we een Rect object r aan met de zojuist berekende afmetingen en in regel 45 voegen we dit Rect object toe aan de dictionary onder de key 'rect'. Let op, key in regel 45 is de key van het huidige element in de loop. Wanneer de loop met het element 'fire' bezig is, worden de afmetingen dus opgeslagen in elements['fire'][rect].
Voeg aan je hoofdprogramma de functieaanroep calc_card_rects() toe en een print statement om te controleren of de rechthoeken goed aan de dictionary zijn toegevoegd. Je hoofdprogramma ziet er dan zo uit:
63# MAIN PROGRAM
64
65load_elements()
66calc_card_rects()
67print(elements)
De output in de console zou er zo moeten uitzien:
Elk element in de dictionary heeft nu een label , een image en een rect veld. Het element 'earth' heeft bijvoorbeeld de volgende structuur:
'earth': {
'label': 'aarde',
'image': 'earth',
'rect': <Rect(0, 0, 102, 38)>
}
Voor dit element hebben we dus een rechthoek van 102 x 38 pixels nodig.
Pretty print
De output in de console ziet er tamelijk chaotisch uit. Dat komt omdat de dictionary in één lange regel wordt geprint. Om de dictionary overzichtelijker weer te geven, kun je de module pprint gebruiken. Doe daartoe het volgende:
Voeg pprint toe aan de import statement op regel 6.
6import json, pygame, pprint
Vervang het print statement in je hoofdprogramma door pprint.pp(elements):
64# MAIN PROGRAM
65
66load_elements()
67calc_card_rects()
68pprint.pp(elements)
De output in de console is nu een stuk overzichtelijker:
Tekenen#
Alle informatie voor de element cards is nu beschikbaar in de elements dictionary. Breid de draw_element_card() als volgt uit om de gehele card te tekenen:
48def draw_element_card(element_id, pos, bordercolor='azure4', bgcolor='white', fontcolor='black'):
49 element = elements[element_id]
50 lbl = element['label']
51 img_name = element['image']
52 rect = element['rect']
53 img = pygame.transform.scale(eval(f'images.{img_name}'), (ICONSIZE, ICONSIZE))
54 rect.topleft = pos
55 screen.draw.filled_rect(rect, bgcolor)
56 screen.draw.rect(rect, bordercolor)
57 screen.blit(img, (rect.left + LEFTMARGIN, rect.top + TOPMARGIN))
58 screen.draw.text(lbl, midleft = (rect.midleft[0] + LEFTMARGIN + ICONSIZE + HSPACE, rect.midleft[1]), fontsize=FONTSIZE, color=fontcolor)
In regel 49 zie je dat de parameters bordercolor, bgcolor en fontcolor zijn toegevoegd. Dit zijn de kleuren van de rand, de achtergrond en de tekst van de element card. Ze hebben alle drie een default waarde gekregen, die wordt gebruikt als in de aanroep het argument ontbreekt.
In de regels 51 t/m 53 halen we de gegevens van het element op uit de dictionary.
In regel 54 stellen we de positie van de rechthoek in op de positie die is meegegeven in de aanroep van draw_element_card().
Tenslotte tekenen we in de regels 55 t/m 59 de rechthoek, de afbeelding en de tekst. De screen.draw.filled_rect() functie tekent de rechthoek met de achtergrondkleur, de screen.draw.rect() functie tekent de rand van de rechthoek en de screen.blit() functie tekent het plaatje. De screen.draw.text() functie tekent de tekst in het rechthoek.
Kijk nog even goed naar de positionering van de tekst in regel 59. Met rect.midleft[0] krijgen we de x-coördinaat van het midden van de linkerkant van de rechthoek en met rect.midleft[1] de y-coördinaat.
Om de element card beter zichtbaar te maken, stellen we in de draw() functie de achtergrondkleur in op azure:
60def draw():
61 screen.fill('azure')
62 draw_element_card('earth', (20,20))
Run de code en kijk of de element card van aarde goed wordt getekend.