Γραφικά υπολογιστών Εργαστήριο 10 Εισαγωγή στα Sprites Σκοπός της 10ης άσκησης είναι να μάθουμε να χρησιμοποιούμε sprites και να φτιάξουμε ένα παιχνίδι που χρησιμοποιεί συγκρούσεις. Θα δούμε επίσης μερικά παραδείγματα που θα μας βοηθήσουν να φτιάξουμε τα δικά μας παιχνίδια στη συνέχεια. 1. Εισαγωγή Τα παιχνίδια μας χρειάζονται υποστήριξη για το χειρισμό αντικειμένων που συγκρούονται, π.χ., μπάλες που αναπηδούν, ακτίνες λέιζερ ή τον χαρακτήρα του παιχνιδιού να μαζεύει ένα νόμισμα. Όλες αυτές οι ενέργειες απαιτούν ανίχνευση συγκρούσεων. Η βιβλιοθήκη Pygame έχει υποστήριξη για sprites. Ένα sprite είναι μια 2Δ εικόνα, η οποία αποτελεί μέρος μιας μεγαλύτερης γραφικής σκηνής. Τυπικά, ένα sprite είναι ένα είδος αντικειμένου με το οποίο θα υπάρχει κάποια αλληλεπίδραση, π.χ., ένα αυτοκίνητο, ένας βάτραχος, ένας χαρακτήρας π.χ. ο Mario. 2. Βασική λειτουργία των sprites και συγκρούσεις Ας ξεκινήσουμε με ένα παράδειγμα προγράμματος που χρησιμοποιεί sprites. Στο παράδειγμα που θα κατεβάσετε από το eclass με όνομα sprite_collect_blocks.py, θα δείτε πώς να δημιουργήσετε μια σκηνή από μάυρα κουτάκια τα οποία θα μπορείτε να τα μαζεύετε κατευθύνοντας κατάλληλα ένα κόκκινο κουτάκι με τη χρήση του ποντικιού. Το πρόγραμμα θα κρατάει και ένα score με τον αριθμό από τα κουτάκια που έχουν συλλεχθεί, το οποίο και θα εμφανίζεται στην κονσόλα. Δοκιμάστε να τρέξετε το παιχνίδι! Στη συνέχεια, θα προσπαθήσουμε να ερμηνέυσουμε τον κώδικα. Το πρόγραμμα αρχίζεί όπως και τα περισσότερα προγραμμάτα που έχουμε φτιάξει εώς τώρα: Η βιβλιοθήκη random θα χρησιμοποιηθεί για την τοποθέτηση των μαύρων κουτιών στις θέσεις τους και στη συνέχεια ορίζονται τα τρία χρώματα που χρησιμοποιούνται από το πρόγραμμα. Στη συνέχεια ορίζεται η κλάση Block. Παρατηρήστε ότι είναι μια υποκλάση της Sprite. Έτσι, η κλάση Block θα διαθέτει όλη τη λειτουργικότητα της κλάσης Sprite. Στη συνέχεια, όπως σε κάθε κλάση, ο constructor της Block δέχεται σαν παράμετρο το self, όπως και κάθε άλλος constructor. Επίσης, δέχεται σαν παραμέτρους και τις color, width και height, οι
οποίες καθορίζουν το χρώμα, το μήκος και το πλάτος του αντικειμένου. Είναι πολύ σημαντικό, να μην παραλείψουμε να καλέσουμε την πατρική κλάση Sprite, προκειμένου να αρχικοποιηθούν τα sprites. Οι δύο τελευταίες γραμμές είναι αυτές που θα δημιουργήσουν την εικόνα που εμφανίζεται στην οθόνη. Η πρώτη δημιουργεί μια κενή εικόνα, ενώ η δεύτερη τη γεμίζει με μαύρο χρώμα. Εάν το πρόγραμμα χρειάζεται κάτι άλλο πέρα από ένα μαύρο τετράγωνο, αυτές είναι οι γραμμές κώδικα που χρειάζεται να τροποποιηθούν. Για να το καταλάβετε αυτό καλύτερα, ρίξτε μια ματιά στον ακόλουθο κώδικα: Εάν τον χρησιμοποιήσετε, τότε θα ζωγραφίζετε ελλείψεις. Αντίθετα, εάν θέλετε κάποιο bitmap γραφικό, τότε μπορείτε να χρησιμοποιήσετε κάτι τέτοιο: Παρατηρήστε ότι στην περίπτωση αυτή δεν χρειάζονται οι διαστάσεις, αντίθετα το μέγεθος του sprite θα προσαρμοστεί σε αυτό του γραφικού bitmap που θα φορτώσετε. Τέλος, σε κάθε περίπτωση χρειάζεται το παρακάτω: Η ιδιότητα rect είναι μια μεταβλητή, η οποία αποτελεί ένα στιγμιότυπο της κλάσης Rect που παρέχεται από το Pygame. Το ορθογώνιο αυτό αναπαριστά τις διαστάσεις του sprite. Η κλάση αυτή έχει ιδιότητες για x και y που αντιπροσωπεύουν το σημείο στο οποίο θα ζωγραφιστεί το sprite. Άρα για να μετακινηθεί το sprite (έστω myspriteref), θα πρέπει να αλλάξουμε τα
myspriteref.rect.x και myspriteref.rect.y. Πλέον έχουμε τελειώσει με την κλάση Block. Ας προχωρήσουμε στον κώδικα για την αρχικοποίηση. Έπειτα από την αρχικοποίηση του Pygame, ορίζονται οι διαστάσεις του παραθύρου που θα δημιουργηθεί και στο οποίο θα τρέχει το παιχνίδι. Δεν υπάρχει κάτι καινούριο σε σχέση με τα προηγούμενα προγράμματα. Ένα από τα πλεονεκτήματα των sprites είναι η δυνατότητα που έχουν να λειτουργούν ως ομάδες. Μπορούμε με μια εντολή να ζωγραφίσουμε και να μετακινήσουμε όλα τα sprites που βρίσκονται σε μια ομάδα. Μπορούμε επίσης να ελέγξουμε για συγκρούσεις sprites ως προς μια ομάδα. Ο παραπάνω κώδικας δημιουργεί δύο λίστες. Η all_sprites_list θα περιέχει κάθε sprite του παιχνιδιού. Αυτή η λίστα θα χρησιμοποιείται για να ζωγραφίζει όλα τα sprites του παιχνιδιού. Η block_list χρησιμοποιείται για να αποθηκεύει όλα τα αντικείμενα με τα οποία μπορεί να συγκρουστεί ο παίκτης. Στο παράδειγμά μας αυτά θα είναι τα μαύρα κουτάκια. Προσοχή, αν βάζαμε και τον παίκτη σε αυτή τη λίστα τότε πάντα θα ανιχνευόταν σύγκρουση με τον εαυτό του! Το loop αυτό δημιουργεί 50 μαύρα κουτάκια. Για κάθε ένα από αυτά καθορίζεται το χρώμα, το μήκος και το πλάτος. Στη συνέχεια δημιουργούνται με τυχαίο τρόπο οι συντεταγμένες στις οποίες θα εμφανιστεί το αντικείμενο αυτό. Τέλος το κουτάκι αυτό μπαίνει στη λίστα των αντικειμένων με τα οποία θα μπορεί να συγκρουστεί ο παίκτης, ο οποίος είναι ένα κόκκινο κουτάκι, το οποίο και δημιουργείται ως εξής: Προσέξτε ότι δεν προστίθεται στη λίστα block_list. Στη συνέχεια ο ακόλουθος κώδικας είναι το σύνηθες loop που χρησιμοποιούμε στα προγράμματά μας. Προσέξτε ότι αρχικοποιεί το score στο 0.
Στη συνέχεια και με παρόμοιο τρόπο όπως έχουμε ήδη δει, ανιχνεύεται η θέση του δείκτη του ποντικιού και αποθηκεύεται στην pos. Έπειτα το sprite (μέσω του rectangle που το περικλείει) μεταφέρεται στη νέα θέση του, που εδώ είναι η νέα θέση του ποντικιού. Με τον ακόλουθο κώδικα γίνεται ανίχνευση της σύγκρουσης του παίκτη με κάποιο από τα αντικείμενα της block_list (τα μαύρα κουτάκια): Πιο συγκεκριμένα, αυτή γραμμή παίρνει το sprite στο οποίο αναφερόμαστε ως player και ελέγχει εάν έχει συγκρουστεί με οποιοδήποτε από τα sprites της block_list. Στην blocks_hit_list επιστρέφονται τα sprites που πέφτουν το ένα πάνω στο άλλο, έχουν δηλαδή κάποια επικάλυψη, άρα συγκρούονται. Σε περίπτωση που δεν υπάρχει κάποια επικάλυψη, η blocks_hit_list είναι άδεια. Η boolean True ορίζει ότι σε περίπτωση σύγκρουσης, τα αντίστοιχα sprites θα απομακρύνονται από την block_list, διαφορετικά και εάν γίνει False, δεν θα απομακρύνονται. Καθώς θέλουμε να μετράμε το score ανάλογα με το πόσα αντικείμενα πετυχαίνει ο παίκτης χρειάζεται με ένα loop να κάνουμε το σχετικό έλεγχο: Αυτό το loop τρέχει για κάθε sprite στην λίστα συγκρούσεων blocks_hit_list που κατασκευάσαμε πιο πριν. Εάν υπάρχουν sprites στη λίστα αυτή, τότε καθένα από αυτά αυξάνει το score κατά 1. To score έπειτα τυπώνεται στην κονσόλα. Η κλάση Group της οποίας μέλος είναι η all_sprites_list έχει μια μέθοδο που ονομάζεται draw. Αυτή η μέθοδος εκτελεί ένα loop σε κάθε sprite της λίστας και καλεί την αντίστοιχη draw του καθενός sprite. Έτσι, με μια γραμμή κώδικα κάθε sprite στην all_sprites_list ζωγραφίζεται.
Τέλος, ο ακόλουθος κώδικας ανανεώνει όπως συνήθως την εικόνα 60 φορές το δευτερόλεπτο και ακολούθως όταν το πρόγραμμα βγει από το βρόχο, τερματίζεται. 3. Κινούμενα sprites Στο παράδειγμά μας μέχρι τώρα, ο παίκτης είναι το μόνο sprite που κινείται. Θα θέλαμε να κάνουμε όλα τα sprites να κινούνται. Αυτό είναι κάτι εύκολο, αρκεί να ακολουθήσουμε τα ακόλουθα δύο βήματα. Το πρώτο βήμα είναι να προσθέσουμε μια καινούρια μέθοδο στην κλάση Block, η οποία και θα ονομάζεται update. Η update θα καλείται αυτόματα για ολόκληρη τη λίστα. Τοποθετείστε τον ακόλουθο κώδικα στο sprite: Τοποθετείστε τον ακόλουθο κώδικα στο κυρίως loop του προγράμματος: Τα κουτάκια θα πέφτουν προς τα κάτω, αλλά δεν θα επανεμφανίζονται. Ο ακόλουθος κώδικας βελτιώνει την update, έτσι ώστε να επανεμφανίζονται από το πάνω μέρος: Εάν θέλαμε τα κουτάκια που έχει μαζέψει ο παίκτης να επανεμφανίζονται στο πάνω μέρος θα προσθέταμε τον ακόλουθο κώδικα:
Και φυσικά θα έπρεπε να γίνει τροποποίηση της ανίχνευσης σύγκρουσης έτσι ώστε να καλείται η reset_pos χωρίς να καταστρέφεται το μαύρο κουτάκι: 4. Η κλάση Game και άλλες κλάσεις. Στο eclass του μαθήματος θα βρείτε μια κλάση Game, την οποία και μπορείτε προαιρετικά να χρησιμοποιήσετε (αφού την μελετήσετε) για την καλύτερη οργάνωση του κώδικά σας. Θα βρείτε επίσης και παραδείγματα που το ανθρωπάκι πυροβολεί, που υπάρχουν τοίχοι, λαβύρινθοι, πλατφόρμες, ένα παιχνίδι με φιδάκι, και τον τρόπο χρήσης sprite sheets (τι είναι αυτά πάλι;).