Debugging κώδικα με τη Visual C# 2008 Express Edition (μέρος 1ο)
Δημοσιεύθηκε από Κώστας Αναγνώστου στο Ιουλίου 28, 2009
Πριν προχωρήσουμε σε περισσότερο σύνθετα παραδείγματα παιχνιδιών, καλό θα ήταν να κάνουμε σήμερα μια παύση και να αναφερθούμε λίγο σε μερικά εργαλεία που μας παρέχει το περιβάλλον της Visual C# 2008 EE, πάνω στο οποίο βασίζεται το XNA Game Studio, για debugging κώδικα. Είναι βασικό για κάθε προγραμματιστή να έχει καλή γνώση του περιβάλλοντος στο οποίο αναπτύσσει μια εφαρμογή και ιδιαίτερα την υποστήριξη για debugging που του προσφέρει. Χρόνος που δαπανάται για τον εντοπισμό ενός σφάλματος, πολλά από τα οποία είναι ανόητες παραλήψεις εκ μέρους του προγραμματιστή (πιστέψτε με, έχω μεγάλη εμπειρία σε αυτό!), είναι χρόνος που αφαιρείται από την ανάπτυξη κώδικα. Οπότε ο χρόνος που σπαταλάμε στο debugging πρέπει να είναι το δυνατόν σύντομος.
Σε σχέση με παλαιότερες γλώσσες όπως τη C/C++, η C#/.NET παρέχει εκ σχεδιασμού πολλές δικλείδες ασφαλείας που αποτρέπουν πολλά από τα κοινά σφάλματα που βρίσκουν το δρόμο τους στο κώδικα, όπως για παράδειγμα η χρήση μιας μεταβλητής που δεν έχει αρχικοποιηθεί (στη C/C++ ιδίως αν η μεταβλητή αυτή είναι pointer μπορεί να οδηγηθεί ο προγραμματιστής σε πολλές άυπνες νύχτες αναζήτησης). Στα σφάλματα λογικής όμως δεν μπορεί να βοηθήσει και πολύ το περιβάλλον, για το λόγο αυτό παρέχει στο προγραμματιστή μηχανισμούς με τους οποίους μπορεί να σταματήσει τη ροή εκτέλεσης και να δει βήμα-βήμα τι λειτουργίες πραγματοποιούνται και τις ενδιάμεσες τιμές των μεταβλητών του προγράμματος. Ένας χρήσιμος μηχανισμός για την εργασία αυτή είναι τα breakpoints.
Θα δούμε τη χρήση των μηχανισμών αυτών με ένα παράδειγμα. Τρέχουμε τη Visual C# και φορτώνουμε την έκδοση του παιχνιδιού Pong που περιγράψαμε στο 4ο μέρος του tutorial. Στην συνέχεια, στη μέθοδο Initialise(), αλλάζουμε τη τιμή της μεταβλητής rightPaddleSpeed σε 15.0f:
protected override void Initialize()
{
//υπόλοιπος κώδικας
rightPaddleSpeed = 15.0f;
base.Initialize();
}
Τρέχουμε το παιχνίδι με F5 και παρατηρούμε ότι η κίνηση της ρακέτας του υπολογιστή είναι κάπως περίεργη, κάνει μια «σπαστική» κίνηση πάνω-κάτω. Έχουμε εξηγήσει γιατί συμβαίνει αυτό, αλλά ας υποθέσουμε ότι δεν ξέρουμε και ότι θέλουμε να το διαπιστώσουμε. Εφόσον το σφάλμα αφορά τη κίνηση της ρακέτας, εντοπίζουμε στην μέθοδο Update το κομμάτι κώδικα που υπολογίζει την κίνηση της και εστιάζουμε εκεί. Ο υπολογισμός της μεταβλητής rightPaddle.Y που καθορίζει τη θέσης ρακέτας του υπολογιστή γίνεται στην γραμμή 137 (περίπου). Τοποθετώντας το κέρσορα στην αμέσως επόμενη γραμμή αυτή και πατώντας F9, ή εναλλακτικά κάνοντας αριστερό κλικ στη μπεζ στα αριστερά της εντολής λωρίδα (στη περιοχή που έχουμε μαρκάρει με κόκκινο στην παρακάτω εικόνα) δημιουργούμε ένα breakpoint στο σημείο αυτό.

Σημείωση: τοποθετήσαμε το breakpoint στην αμέσως επόμενη γραμμή από αυτή που μας ενδιαφέρει, γιατί θέλουμε να μάθουμε τη τιμή της rightPaddle.Y, αφότου η εντολή υπολογισμού της έχει εκτελεστεί.
Το breakpoint συμβολίζεται με ένα κόκκινο κύκλο στο ύψος της γραμμής που επιθυμούμε να σταματήσει η εκτέλεση του προγράμματος (το πρόγραμμα θα εισέλθει σε κατάσταση break mode):

Αν το πρόγραμμα εκτελείται την στιγμή που το κάνουμε αυτό, τότε η εκτέλεση θα σταματήσει στο σημείο αυτό, και ένα κίτρινο βέλος θα εμφανιστεί πάνω στο breakpoint. Αν δεν τρέχουμε το πρόγραμμα μπορούμε και πάλι να τοποθετήσουμε το breakpoint στο σημείο αυτό με τον τρόπο που περιγράψαμε και να πατήσουμε F5 για να αρχίσει η εκτέλεση.

Από τη στιγμή που το πρόγραμμα μπει σε break mode, έχουμε τη δυνατότητα να ελέγξουμε τις τιμές των μεταβλητών του. Στην συγκεκριμένη περίπτωση, αν τοποθετήσουμε το κέρσορα πάνω από το όνομα «rightPaddle.Y», θα μάθουμε την τιμή της μεταβλητής αυτής. Το να τοποθετούμε το κέρσορα πάνω από το όνομα της μεταβλητής για να μάθουμε τη τιμή της είναι ένας έγκυρος τρόπος, αλλά όχι ο πιο εύχρηστος. Υπάρχουν και άλλοι τρόποι που διευκολύνουν την εργασία αυτή, ιδιαίτερα αν πρέπει να ελέγχουμε τις τιμές μιας μεταβλητής σε πολλές επαναλήψεις του προγράμματος. Επιλέγουμε/μαρκάρουμε λοιπόν το όνομα της μεταβλητής rightPaddle.Y και πατάμε το δεξί-κλικ επάνω της (ΠΡΟΣΟΧΗ! Επιλέγουμε όλο το όνομα και όχι μόνο το Y. To Y χωρίς το πρόθεμα rightPaddle δεν έχει νόημα). Στο μενού που εμφανίζεται επιλέγουμε Add Watch.

Ένα νέο παράθυρο εμφανίζεται στο κάτω μέρος της οθόνης, με το όνομα Watch (αν δεν υπάρχει ήδη, μπορεί να γίνει ορατό επιλέγοντας από το κυρίως μενού Debug/Windows/Watch).

Στο παράθυρο αυτό βλέπουμε τώρα στη δεξιά στήλη το όνομα (Name) της μεταβλητής στο μεσαίο την τιμή της (Value) και στο αριστερό το τύπο της μεταβλητής (Type). Η μεταβλητή αυτή θα μείνει μόνιμα στη λίστα μέχρι να την αφαιρέσουμε εμείς. Στο παράθυρο Watch μπορούμε να προσθέσουμε όσες μεταβλητές θέλουμε εμείς, με τον τρόπο που περιγράψαμε παραπάνω. Σε περίπτωση που κάποια μεταβλητή δεν εμφανίζει την τιμή της αυτό σημαίνει ότι δεν είναι ορατή σε αυτό το σημείο και ότι έχει οριστεί αλλού, σε κάποια άλλη μέθοδο ή και αντικείμενο.
Από τη στιγμή που βρισκόμαστε σε break mode, αν πατήσουμε το πλήκτρο F5 η εκτέλεση του κώδικα θα συνεχιστεί μέχρις ότου να συναντήσει το επόμενο breakpoint (αν υπάρχει). Στην περίπτωση μας που έχουμε ορίσει μόνο ένα breakpoint, το πρόγραμμα θα κάνει μια πλήρη επανάληψη και θα καταλήξει στο ίδιο σημείο και πάλι (το ΧΝΑ θα καλέσει ξανά την Update δηλαδή και θα σταματήσει στο breakpoint). Αυτή τη φορά η τιμή της rightPaddle.Y όπως τη βλέπουμε στο παράθυρο Watch είναι διαφορετική. Δοκιμάστε να πατήσετε το F5 μερικές φορές ακόμα να δείτε πως αλλάζει η τιμή της στη πάροδο του χρόνου. Παρατηρούμε ότι η τιμή της αυξάνει για μερικές επαναλήψεις, μειώνεται για μια επανάληψη και συνεχίζει να αυξάνεται πάλι. Αυτό συμβαίνει περιοδικά και δικαιολογεί τη «σπαστική» κίνηση της ρακέτας πάνω-κάτω. Για να καταλάβουμε γιατί συμβαίνει αυτό πρέπει συγκρίνουμε τις Υ τιμές των κέντρων της μπάλας και της ρακέτας, μιας και η διαφορά τους καθορίζει τη τιμή της μεταβλητής sign και το αν θα αυξηθεί ή θα μειωθεί η rightPaddle.Y.
//κινηση ρακέτας υπολογιστή
int sign = Math.Sign((ball.Top + ball.Height / 2) - (rightPaddle.Top + rightPaddle.Height / 2));
rightPaddle.Y += (int)(sign*rightPaddleSpeed);
Στο παράθυροo Watch μπορώ να προσθέσω εκτός από ονόματα μεταβλητών, και εκφράσεις. Το Visual Studio θα υπολογίσει τη τιμή τους και θα την απεικονίσει στο παράθυρο. Επιλέγω λοιπόν τις εκφράσεις ball.Top + ball.Height / 2 και rightPaddle.Top + rightPaddle.Height / 2 και τις κάνω κανονικά Add Watch (ή με drag and drop στο παράθυρο Watch) κανονικά σαν να ήταν μεταβλητές. Στο παράθυρο βλέπω τώρα τις τιμές των εκφράσεων αυτών.

Πατώντας τώρα μερικές φορές το F5 και παρατηρώντας τις τιμές των εκφράσεων (τη Υ συνταταγμένη των αντίστοιχων κέντρων δηλαδή) σε κάθε επανάληψη βλέπουμε ότι το κέντρο της ρακέτας rightPaddle.Top + rightPaddle.Height / 2 αυξάνει σε τιμή γρηγορότερα από το κέντρο της μπάλας ball.Top + ball.Height / 2, με αποτέλεσμα η ρακέτα να προσπερνά την μπάλα και να προσπαθεί να επιστρέψει ώστε να έρθει στο σωστό ύψος σε σχέση με αυτή.
Παρατηρώντας λοιπόν τις τιμές των μεταβλητών με κατάλληλα τοποθετημένα breakpoints μπορούμε να εντοπίσουμε προβλήματα «λογικής» σαν και αυτά. Όντας σε break-mode (η εκτέλεση του προγράμματος έχει παγώσει δηλαδή), μπορούμε εκτός του F5 να χρησιμοποιήσουμε και το πλήκτρο F10, το οποίο θα μεταφέρει την εκτέλεση στην επόμενη εντολή, δίνοντας μας την δυνατότητα να τρέξουμε το πρόγραμμα γραμμή-γραμμή παρατηρώντας την αλλαγή των τιμών διάφορων μεταβλητών (step execution). Τέλος ένα άλλο χρήσιμο πλήκτρο όσο κάνουμε debugging το πρόγραμμα είναι και το F9 το οποίο, αν η επόμενη εντολή είναι μέθοδος, θα προσπαθήσει να «μπει» μέσα σε αυτή και θα μας δείξει ποιες εντολές εκτελούνται εκεί. Στη συγκεκριμένη περίπτωση δεν έχει εφαρμογή αλλά είναι χρήσιμο να γνωρίζουμε για αυτή.
Δυστυχώς η Express έκδοση του Visual Studio (που χρησιμοποιούμε εμείς) δεν δίνει επιπλέον ευελιξία στα breakpoints. Η κανονική (εμπορική) έκδοση του Visual Studio επιτρέπει τον ορισμό συνθηκών για το πότε θα σταματά η εκτέλεση σε ένα breakpoint όπως για παράδειγμα ένα μετρητή που να ορίζει πόσες επαναλήψεις πρέπει να πραγματοποιηθούν μέχρι να σταματήσει η εκτέλεση στο σημείο αυτό, ή ακόμα και if-συνθήκες που καθορίζουν πότε θα ενεργοποιηθεί το breakpoint και πότε όχι. Αλλά ακόμα και έτσι, η δυνατότητα να σταματώ την εκτέλεση του προγράμματος και να ελέγχω βήμα-βήμα το τι λειτουργίες εκτελούνται και ποιο το αποτέλεσμα τους είναι σημαντικό εργαλείο ενάντια στο πόλεμο με τα σφάλματα κώδικα.
Στο επόμενο tutorial θα συνεχίσουμε το debugging του προγράμματος με επιπλέον (των breakpoints) μηχανισμών που παρέχει η C#/.NET.

darklynx είπε
Ωραία
.Δεν θα ήταν άσχημο στο 2ο άρθρο να δούμε και debugging για Xbox360 αν αυτό είναι δυνατόν…
Κώστας Αναγνώστου είπε
Δεν έχει καμία διαφορά! Δεν θέλω να παινέσω τη Microsoft, αλλά το να αναπτύσσεις παιχνίδια για το Xbox360, ακόμα και χωρίς το XNA Game Studio, δεν έχει καμία διαφορά (ως διαδικασία) με το να αναπτύσσεις για PC. Μπορείς να τρέξεις το παιχνίδι μέσω του Visual Studio (που τρέχει στο PC) απευθείας στο Xbox360, να θέσεις breakpoints και τα λοιπά και να κάνεις debugging στο παιχνίδι σαν να έτρεχε σε PC. Πολύ εντυπωσιακό θα έλεγα.
Στο επόμενο άρθρο θα ασχοληθούμε περισσότερο με Asserts, Exceptions, Call Stack κλπ.