Το προφίλ μας στο Google Plus
0

Python Game Programming (part 3.5)

Ελπίζουμε να μη ζαλιστήκατε (πολύ) με τις τόσες νέες έννοιες στο τρίτο άρθρο της σειράς μας, που δημοσιεύτηκε στο deltaHacker 007! Αντικείμενα, συμβάντα, βιβλιοθήκη pygame, μέθοδοι — μέχρι και φυσική Λυκείου είχαμε! Αλλά φυσικά, μπρος στα graphics τι είν’ ο πόνος. Σίγουρα είστε διατεθειμένοι να κουραστείτε λίγο ακόμα — εξάλλου πλησιάζει η ώρα για το πρώτο μας πραγματικό παιχνίδι γραφικών. Και φυσικά, δεν χρειάζεται να μας το πείτε: το ξέρουμε ότι είστε ήδη εθισμένοι με την Python, τον προγραμματισμό, το pygame και όλα τ’ άλλα που ίσως δεν πολυκαταλαβαίνετε ακόμα. Αλλά δεν πειράζει: Το ξενύχτι είναι μέρος της μαγείας! Πάμε λοιπόν να εξερευνήσουμε τον αντικειμενοστραφή προγραμματισμό.

Ξεκινάμε ευθύς αμέσως με τον αντικειμενοστρεφή προγραμματισμό για πρωτάρηδες ή “for dummies”, όπως θα λέγαμε αν κρατούσαμε τον Αγγλικό τίτλο της δημοφιλούς σειράς βιβλίων. Βέβαια οι αναγνώστες μας δεν είναι dummies, κάθε άλλο μάλιστα: dummies είναι όσοι χρησιμοποιούν *μόνον* ετοιματζίδικα πράγματα!

Είναι σχετικά δύσκολο να πάρετε μια ικανοποιητική απάντηση στην ερώτηση “τι είναι αντικειμενοστραφής προγραμματισμός”. Είναι μάλλον πιο εύκολο να καταλάβει κανείς μέσα από τα προγράμματα, παρά να προσπαθεί να εξηγήσει με θεωρητικούς ορισμούς. Ωστόσο, οι περισσότεροι συμφωνούμε στα ακόλουθα.

Ένα αντικείμενο (object) είναι μια συλλογή από δεδομένα –μπορείτε να τα πείτε και ιδιότητες (properties) ή χαρακτηριστικά (attributes)–, με την ιδιαιτερότητα ότι ενσωματώνει τις δικές του μεθόδους (methods) για να τα χειρίζεται.

Προκειμένου να δημιουργήσουμε δικά μας αντικείμενα, τα οποία θα περιέχουν τα δεδομένα και τις μεθόδους της επιλογής μας, γράφουμε μια κλάση (class). Σε αυτή περιγράφουμε τα δεδομένα που θα περιέχει το αντικείμενό μας καθώς και τις μεθόδους που τα χειρίζονται. Όταν δημιουργήσουμε ένα αντικείμενο που ανήκει στην κλάση μας, αυτόματα θα διαθέτει τα χαρακτηριστικά και τις μεθόδους που ορίσαμε.

Δεν είναι όμως μόνο αυτό. Στο κάτω κάτω, θα μπορούσατε να σκεφτείτε ότι δεν αλλάζει κάτι ιδιαίτερα: Αντί να έχουμε συναρτήσεις και να τους δίνουμε τα δεδομένα μας, τα ίδια τα δεδομένα μας έχουν τις δικές τους συναρτήσεις. Ε, και λοιπόν; Θα σας πούμε τι “ε, και λοιπόν”. Ο αντικειμενοστραφής προγραμματισμός, που λέτε, έχει ακόμα τρεις βασικές έννοιες:

Πολυμορφία (polymorphism). Φανταστείτε δύο αντικείμενα από δύο διαφορετικές κλάσεις, να διαθέτουν μια μέθοδο με το ίδιο όνομα. Ανάλογα με το αντικείμενο που χρησιμοποιείται όταν γίνεται η κλήση της μεθόδου, καλείται κάθε φορά η σωστή!
Ενθυλάκωση (encapsulation). Άλλο πράγμα το να γράψεις πώς δουλεύει ένα αντικείμενο (στο κομμάτι που περιγράφεται η κλάση) και άλλο να το χρησιμοποιείς μετά για το κανονικό σου πρόγραμμα. Κατά τη χρήση, δεν μας ενδιαφέρει πλέον το πώς λειτουργεί εσωτερικά το αντικείμενο: αρκεί να κάνει αυτό που θέλουμε! Η ενθυλάκωση κρύβει την εσωτερική πολυπλοκότητα του αντικειμένου, όταν πλέον δεν χρειάζεται να την βλέπουμε. Σκεφτείτε, π.χ., το εξής: Δεν ξέρετε πως λειτουργεί εσωτερικά η επιφάνεια (surface) του pygame. Μπορείτε όμως να τη χρησιμοποιήσετε γιατί το μόνο που χρειάζεται να ξέρετε είναι οι λειτουργίες που σας παρέχουν οι μέθοδοι.
Κληρονομικότητα (inheritance). Με αυτήν την καταπληκτική δυνατότητα μπορείτε να ξεκινήσετε δημιουργώντας μια γενική κλάση αντικειμένων κι από αυτή να παράγετε πιο εξειδικευμένες κλάσεις, με πρόσθετα χαρακτηριστικά που να κληρονομούν όμως τα χαρακτηριστικά και τις μεθόδους της γονικής τους κλάσης.

Ειδικά η κληρονομικότητα σε συνδυασμό με την πολυμορφία, μας επιτρέπουν να κάνουμε ιδιαίτερα καλά κόλπα. Όμως αντί να σας τα λέμε θεωρητικά, θα σας τα δείξουμε με παραδείγματα. Όχι τίποτε άλλο, δηλαδή, αλλά κάποιοι από εσάς αναμφίβολα μένετε σε …ψηλούς ορόφους και δεν μπορούμε να συνεχίσουμε άλλο με τη θεωρία. Και πραγματικά, αν κάπου μας χάσατε με τα παραπάνω μην ανησυχείτε: όλα θα ξεκαθαρίσουν στην πορεία!

Θεός για μια ημέρα!
Φανταστείτε ότι είστε ο θεός και πρόκειται να δημιουργήσετε τη ζωή πάνω στη Γη — σε Python, βέβαια! Έχετε ήδη ασχοληθεί με φυτά και άλλα τέτοια βαρετά κι έχετε φτάσει στα θηλαστικά. Σκέφτεστε λοιπόν να φτιάξετε μια κλάση θηλαστικών κι από αυτή να παράγετε ό,τι άλλο χρειάζεστε (γάτες, σκύλους, ανθρώπους κ.ο.κ.). Προφανώς, ένα αντικείμενο που ανήκει στην κλάση “θηλαστικό” έχει διάφορα χαρακτηριστικά (attributes) και λειτουργίες (μεθόδους), αλλά για το παράδειγμα μας θα επικεντρωθούμε σε ένα: την ομιλία.

Όλα τα θηλαστικά έχουν κάποιο είδος φωνής: οι σκύλοι γαβγίζουν (bark), οι γάτες νιαουρίζουν (meow) και οι άνθρωποι, ε, μερικές φορές καλύτερα να μασάνε παρά να μιλάνε. Καθώς φαντάζεστε, σαν θεός θα πρέπει να φτιάξετε μια μέθοδο speak, η οποία όταν καλείται να κάνει τη σωστή δουλειά, ανάλογα φυσικά με το ζώο. Στο κάτω κάτω δεν υπάρχουν σκύλοι που να νιαουρίζουν, ούτε γάτες που να γαβγίζουν!

Ξεκινάτε λοιπόν δημιουργώντας μια κλάση που περιγράφει τα θηλαστικά, γενικά:

<br />
class mammal:<br />
	def speak(self):<br />
		print &quot;Mpla mpla!&quot;<br />

Η δημιουργία μιας κλάσης ξεκινά με την εντολή class. Για να γράψουμε μια μέθοδο που ανήκει στην κλάση, τη γράφουμε ως συνάρτηση (με την def, που ξέρουμε ήδη) μέσα στην κλάση. Το self που βλέπετε σημαίνει ότι το πρώτο όρισμα της συνάρτησης είναι ένα αντικείμενο της ίδιας της κλάσης που ορίζουμε. Μπορείτε τώρα να δημιουργήσετε ένα ζώο της γενικής κατηγορίας mammal και να το βάλετε να μιλήσει. Όταν δημιουργούμε ένα αντικείμενο που ανήκει σε μια κλάση λέμε ότι έχουμε φτιάξει ένα στιγμιότυπο (instance) αυτής της κλάσης:

<br />
animal = mammal()<br />
animal.speak()<br />

Το συγκεκριμένο τμήμα κώδικα θα δώσει:

<br />
Mpla mpla!<br />

Βέβαια εμείς δεν ξέρουμε κανένα ζώο που να έχει για φωνή το “μπλα μπλα”, οπότε θα φτιάξουμε τώρα τις ειδικότερες κλάσεις “γάτα” και “σκύλος”, οι οποίες προέρχεται από τη mammal:

<br />
class cat(mammal):<br />
	def speak(self):<br />
		print &quot;Meow!&quot;</p>
<p>class dog(mammal):<br />
	def speak(self):<br />
		print &quot;Bark!&quot;</p>
<p># Δημιουργία γάτας!<br />
thecat = cat()</p>
<p># Δημιουργία σκύλου!<br />
thedog = dog()</p>
<p># Ομιλία!<br />
thecat.speak()<br />
thedog.speak()<br />

Φτιάξαμε τις κλάσεις cat και dog, που προέρχονται από τη mammal (το δηλώσαμε αυτό γράφοντας το όνομα της γονικής κλάσης μέσα στην παρένθεση). Καθεμιά όμως από αυτές τις δύο υλοποιεί ξανά τη μέθοδο speak. Έτσι, όταν καλούμε τη μέθοδο speak μιας γάτας δεν παίρνουμε “Mpla mpla”, αλλά “Meow”. Ομοίως, όταν καλούμε τη μέθοδο speak ενός σκύλου παίρνουμε “Bark”. (Τώρα, είμαι σίγουρος ότι αν ο γείτονάς σας έχει σκύλο που γαβγίζει κάθε βράδυ, ξέρετε ποια είναι η λύση: Να του γράψετε μια μέθοδο shutup και να την καλέσετε!)

Συνήθως δίνουμε ονόματα στις γάτες και τους σκύλους μας, οπότε σαν θεός που είστε θέλετε να το προβλέψετε αυτό στην κλάση σας. Κανένα πρόβλημα. Μόνο που δεν χρειάζεται να το γράψετε χωριστά για το σκύλο και τη γάτα. Βάλτε το απλώς στην κλάση mammal και θα το κληρονομήσουν:

<br />
class mammal:<br />
	def speak(self):<br />
		print &quot;Mpla mpla!&quot;<br />
	def setName(self, name):<br />
		self.name = name<br />
	def getName(self):<br />
		return self.name<br />

Δοκιμάστε προσθέτοντας τις παρακάτω γραμμές στο κύριο πρόγραμμα:

<br />
thecat.setName(&quot;Catia&quot;)<br />
thedog.setName(&quot;Lassie&quot;)<br />
print thecat.getName()<br />
print thedog.getName()<br />

Μπορούμε επίσης να έχουμε απευθείας πρόσβαση στα δεδομένα της κλάσης, χωρίς να χρησιμοποιήσουμε τις συναρτήσεις getName και setName που φτιάξαμε:

<br />
print thedog.name<br />
thecat.name=&quot;Susan&quot;<br />
print thecat.name<br />

Σε πολλές περιπτώσεις, όταν δημιουργούμε ένα αντικείμενο θέλουμε να έχει από την αρχή κάποια δεδομένα ή ιδιότητες. Για παράδειγμα, σαν θεός αποφασίσατε ότι όλες οι γάτες θα γεννιούνται …μαύρες και όλα τα θηλαστικά θα έχουν τέσσερα πόδια! Για να δώσετε αρχική κατάσταση σ’ ένα αντικείμενο κατά τη δημιουργία του, χρειάζεστε μια ειδική συνάρτηση που ονομάζεται κατασκευαστής (constructor). Για το παράδειγμα μας, έχουμε:

<br />
class mammal(object):<br />
	def __init__(self):<br />
		self.legs=4<br />
	def speak(self):<br />
		print &quot;Mpla mpla!&quot;<br />
	def setName(self, name):<br />
		self.name = name<br />
	def getName(self):<br />
		return self.name</p>
<p>class cat(mammal):<br />
	def __init__(self):<br />
		super(cat,self).__init__()<br />
		self.color=&quot;black&quot;<br />
	def speak(self):<br />
		print &quot;Meow!&quot;</p>
<p>thecat = cat()<br />
print thecat.color<br />
print thecat.legs<br />

Προσέξτε τη συνάρτηση __init__, στην κλάση mammal: Ορίζει ότι όλα τα θηλαστικά έχουν τέσσερα πόδια. (Θεός είστε ό,τι θέλετε κάνετε.) Αντίστοιχα, ο constructor για τις γάτες ορίζει ότι το χρώμα τους θα είναι μαύρο. Με μια προσεκτικότερη ματιά θα δείτε ότι το class mammal φαίνεται να προέρχεται πλέον από την γενικότερη κλάση object. Αυτό το χρειαζόμαστε για να λειτουργήσει η συνάρτηση super στον constructor της γάτας — και θα δούμε τώρα τι κάνει αυτή.

Όταν δημιουργείτε μια γάτα, καλείται ο constructor στην κλάση της. Προσέξτε ότι δεν καλείται ο constructor του mammal — όχι αυτόματα, δηλαδή. Σε κάποιες γλώσσες, όταν δημιουργούμε ένα αντικείμενο μιας υποκλάσης (subclass), καλείται αυτόματα πρώτα ο constructor της γονικής κλάσης (superclass). Στην Python αυτό δεν συμβαίνει. Αν θέλουμε να καλέσουμε τον constructor της mammal θα πρέπει να το κάνουμε μέσα από τον constructor της cat. Θα μπορούσε να γίνει ως εξής:

<br />
mamal.__init__(self)<br />

Αλλά ο νέος, Σωστός Τρόπος (TM), είναι:

<br />
super(cat,self).__init__()<br />

Με την super ουσιαστικά λέμε να χρησιμοποιηθεί η κλάση από την οποία προέρχεται η cat, εν προκειμένω η mammal. Ελπίζουμε να ζαλιστήκατε αρκετά με αυτή τη μικρή εισαγωγή στον αντικειμενοστρεφή προγραμματισμό. Στα επόμενα άρθρα θα χρησιμοποιήσουμε αυτές τις γνώσεις στα παιχνίδια μας. Φανταστείτε για παράδειγμα ότι η μπάλα στο παράδειγμα του τεύχους 007 είναι ένα object που γνωρίζει τη θέση του και έχει μια μέθοδο move, για να κινείται! Βάλτε τώρα στο μυαλό σας ότι όλα τα UFO στο Space Invaders είναι απλά αντικείμενα, του invader class!

Παιχνίδια με την Import
Είδαμε στο τρίτο άρθρο της σειράς μας, στο τεύχος 007, ότι μπορούμε να γράψουμε κάτι σαν

<br />
from pygame.locals import *<br />

και μετά να χρησιμοποιήσουμε ό,τι περιέχεται μέσα στο pygame.locals χωρίς να χρειάζεται να γράφουμε κάτι σαν “pygame.μέθοδος”. Σας ρωτήσαμε επίσης γιατί δεν το κάνουμε αυτό πάντα, ώστε να γλυτώσουμε μια και καλή από τόση πληκτρολόγηση. Με βάση την εισαγωγή που κάναμε στο παρόν άρθρο, ίσως είστε σε θέση να το καταλάβετε. Η ίδια μέθοδος ενδέχεται να υπάρχει και σε άλλη βιβλιοθήκη και να το φέρει η …μοίρα, να τις χρησιμοποιούμε και τις δύο. Σε αυτή την περίπτωση ποια θα κληθεί, αφού δεν θα υπάρχει από μπροστά το όνομά της;

Θα πρέπει να ξέρετε ότι τα περιεχόμενα του pygame.locals είναι διαθέσιμα ήδη από την στιγμή που γράφετε import pygame. Η γραμμή που βάλαμε είναι απλώς για να τα κάνουμε import και να τα χρησιμοποιούμε συντομευμένα.

Τα βλέπω διπλά: Δύο bouncing balls!
Στο άρθρο του περιοδικού σάς ζητήσαμε να μετατρέψετε το bouncing ball ώστε να έχει δύο μπάλες. Ελπίζουμε να μη δυσκολευτήκατε. Ας δούμε μια απλοϊκή λύση. Κάτω από την γραμμή

<br />
xspeed,yspeed = 50.0 , 50.0<br />

προσθέστε:

<br />
x2,y2 = 50.0, 50.0<br />
xspeed2,yspeed2 = 30,30<br />

Βάζουμε διαφορετική ταχύτητα, ώστε να καταλαβαίνουμε ποια μπάλα είναι η πρώτη και ποια η δεύτερη. Επίσης, ξεκινάμε και από άλλη αρχική θέση. Ακολούθως δίνουμε όλον τον κύριο βρόχο του προγράμματος (έχουμε αφαιρέσει τα αρχικά σχόλια, για συντομία):

<br />
endprogram = False<br />
while not endprogram:<br />
	screen.fill(surfacecolor)<br />
	screen.blit(ball, (x, y))<br />
	screen.blit(ball, (x2,y2))<br />
	time = clock.tick()<br />
	thetext = textfont.render(str(1000/time), True, (255,0,0),(255,255,0))<br />
	screen.blit(thetext,(0,0))<br />
	time = time / 1000.0<br />
	distance_x = time * xspeed<br />
	distance_y = time * yspeed<br />
	distance_x2 = time * xspeed2<br />
	distance_y2 = time * yspeed2<br />
	x = x + distance_x<br />
	y = y + distance_y<br />
	x2 = x2 + distance_x2<br />
	y2 = y2 + distance_y2<br />
	if (x &gt; (640.0-ballwidth) or x&lt;=0.0):<br />
		xspeed = -xspeed<br />
	if (y &gt; (480.0-ballheight) or y&lt;=0.0):<br />
		yspeed = -yspeed<br />
	if (x2 &gt; (640.0-ballwidth) or x2&lt;=0.0):<br />
		xspeed2 = -xspeed2<br />
	if (y2 &gt; (480.0-ballheight) or y2&lt;=0.0):<br />
		yspeed2 = -yspeed2</p>
<p>	pygame.display.update()<br />
	endprogram = getQuit()</p>
<p>pygame.quit()<br />
exit()<br />

Το παραπάνω τμήμα κώδικα είναι εύκολο, αλλά είναι και κακογραμμένο! Πράγματι, το να βάλουμε μερικές ακόμα μεταβλητές για να φτιάξουμε μια δεύτερη μπάλα μπορεί να είναι απλό, τι θα κάνατε όμως αν σας έλεγα να το τροποποιήσετε για 20 μπάλες; Για 100 μπάλες; Γενικά, για αυθαίρετο αριθμό από μπάλες Θα πρέπει σίγουρα να αλλάξετε τρόπο σκέψης! Σκεφτείτε το μέχρι την επόμενη φορά. Αν βρείτε λύση θα χαρούμε να τη δούμε: Στείλτε μας σ’ ένα σχόλιο το link προς τον κώδικά σας.

Α, και τώρα φυσικά που θα τρέξετε το πρόγραμμα, θα δείτε και ποια μπάλα περνάει πάνω από την άλλη. (Κι αυτό το αποτέλεσμα είναι απόλυτα λογικό.) Επιτρέψτε μου τώρα να κλείσω κάπου εδώ το άρθρο 3.5 και να σας αφήσω να μελετήσετε και να πειραματιστείτε — κατά προτίμηση όλη τη νύχτα! Εγώ με τη σειρά μου, πάω να καλέσω επειγόντως τη συνάρτηση sonic.eat() :D

Leave a Reply

You must be logged in to post a comment.

Σύνδεση

Αρχείο δημοσιεύσεων