Με µαύρο τα κοµµάτια από την εκφώνηση. Με µπλε απαντήσεις κι επεξηγήσεις. Με κόκκινο τα πιο συχνά λάθη που είδαµε. Άσκηση Παρασκευής ΕΡΓΑΣΤΗΡΙΟ 11 - Απαντήσεις Σε αυτή την άσκηση θα γράψετε ένα πρόγραµµα που αποθηκεύει πληροφορίες για τους χαρακτήρες ενός παιχνιδιού (sprites) και τη θέση τους στην οθόνη. Κατασκευάστε ένα struct το οποίο αναπαριστά ένα sprite. Για την ακρίβεια, περιέχει τα πεδία: ζωή (ακέραιος) όνοµα χαρακτήρα (συµβολοσειρά, δεν περιέχει κενά, µέγιστο µέγεθος NAME_SIZE) συντεταγµένες πάνω αριστερά γωνίας (ακέραιοι) συντεταγµένες κάτω δεξιά γωνίας (ακέραιοι) Κατασκευάζουµε το παρακάτω struct: struct spritet { int life; char name[name_size]; int left_x, left_y, right_x, right_y; ; Για το NAME_SIZE βλέπουµε πιο κάτω στην εκφώνηση ότι προτείνεται η τιµή 100, οπότε προσθέτουµε στο πρόγραµµά µας και το απαραίτητο #define. Γράψτε µια συνάρτηση main στην οποία: ορίζετε ένα πίνακα από sprites, µεγέθους MAX_SLOTS Εφόσον χρειαζόµαστε πίνακα για να αποθηκεύσουµε τα στοιχεία των sprites, και ένα sprite αναπαρίσταται από τον τύπο struct sprite που κατασκευάσαµε, θα ορίσουµε πίνακα από struct spritet: struct spriteτ slots[max_slots]; Προσθέτουµε στο πάνω µέρος του αρχείου και το #define για το MAX_SLOTS. Μέχρι να γεµίσει ο πίνακας ή να µην υπάρχουν άλλα δεδοµένα, Εδώ χρειάζεται µια επανάληψη, εφόσον µας λέει "µέχρι να γεµίσει ο πίνακας". Το "δεν υπάρχουν άλλα δεδοµένα" το αφήνουµε προς το παρόν και θα ασχοληθούµε µε αυτό αργότερα. Η ουσιαστική παρατήρηση εδώ είναι ότι το πότε θα τελειώσει η επανάληψη δεν εξαρτάται αποκλειστικά από την τιµή κάποιου µετρητή, αλλά και από κάποιο γεγονός (αν υπάρχουν άλλα δεδοµένα), άρα είναι πιο κατάλληλη η χρήση while ή dowhile. Επιλέγουµε do-while διότι τουλάχιστον µία φορά θα ερωτηθεί ο χρήστης του προγράµµατος και θα τρέξει το πρόγραµµα. Για να ξέρουµε πότε γέµισε ο πίνακας, θα κρατήσουµε ένα µετρητή για το πόσες καταχωρήσεις έχουν γίνει µέχρι στιγµής. Αρχικά έχουµε µηδέν καταχωρήσεις. Άρα, int num_sprites = 0; while (num_sprites < MAX_SLOTS);
Δηµιουργεί µια προσωρινή µεταβλητή τύπου struct sprite στην οποία θα αποθηκεύσει τα παρακάτω δεδοµένα πριν ελέγξει αν πρέπει ή όχι να εισαχθούν στον πίνακα. Πριν εισάγουµε ένα νέο sprite στον πίνακα, πρέπει να έχουµε επιβεβαιώσει ότι δεν υπάρχει επικάλυψη µε άλλο sprite στον χώρο. Εποµένως, δε γίνεται να αποθηκεύουµε τα δεδοµένα απευθείας στον πίνακα. Πρέπει να τα βάλουµε σε µια προσωρινή µεταβλητή, κι αν τελικά δούµε ότι πρέπει να αποθηκευτούν, τα αντιγράφουµε. struct spritet new_sprite; εκτυπώνει το µήνυµα Enter sprite name: και διαβάζει το όνοµα του sprite. Το όνοµα του sprite δεν έχει κενά σύµφωνα µε τις προδιαγραφές που δόθηκαν νωρίτερα, εποµένως µπορούµε να χρησιµοποιήσουµε scanf. Είναι δεκτή και η χρήση fgets, αν θεωρήσουµε ότι κάθε στοιχείο δίνεται σε νέα γραµµή, αλλά πρέπει να προσέξουµε: Πριν την fgets χρειάζεται µια getchar για να καταναλώσει το \n που έχει ξεµείνει από την είσοδο του κωδικού νωρίτερα. Η scanf δε χρειάζεται κάτι τέτοιο. ΠΡΟΣΟΧΗ: Εδώ έγιναν αρκετά λάθη στις ασκήσεις που είδαµε. Διαβάζουµε συµβολοσειρά, εποµένως πρέπει να δώσουµε κατάλληλο format string ώστε να διαβαστεί σωστά και να µην υπάρχει πιθανότητα για buffer overflow. Λάθος 1: scanf("%s", new_sprite.name); Δεν υπάρχει όριο στο format string και µπορεί να διαβαστεί µεγαλύτερο string από όσο χώρο έχουµε στο course_name. Λάθος 2: scanf("%100s", new_sprite.name); Yπάρχει όριο στο format string αλλά είναι λάθος. Επιτρέπει να διαβαστούν µέχρι και 100 χαρακτήρες, το οποίο σηµαίνει ότι το \0 θα αποθηκευτεί στη θέση 101, ένα byte έξω από τα όρια του πίνακα. Λάθος 3: scanf("%s", &new_sprite.name); Η scanf παίρνει ως παράµετρο τη διεύθυνση στην οποία θέλουµε να µας αποθηκεύσει αυτό που διάβασε. Το new_sprite.name είναι ήδη η διεύθυνση στην οποία θα αποθηκευτεί το string, εποµένως δε βάζουµε & Το σωστό είναι να καταστευαστεί ένα format string µε χρήση της sprintf όπως έχουµε δει στο µάθηµα κι έχουµε κάνει σε προηγούµενα εργαστήρια. Κάντε το! εκτυπώνει χαρακτήρα αλλαγής γραµµής και το µήνυµα Enter life points: και διαβάζει τον κωδικό µιας αίθουσας. printf("\nenter life points: "); scanf("%d", &new_sprite.life); εκτυπώνει το µήνυµα Enter bounding box lx,ly-rx,ry: εκτυπώνει το µήνυµα Enter bounding box lx,ly-rx,ry: και διαβάζει τις συντεταγµένες του νοητού ορθογωνίου που περικλείει το sprite. Οι συντεταγµένες δίνονται µε τη µορφή lx,ly-rx,ry όπου lx,ly είναι η πάνω αριστερά γωνία και rx,ry η κάτω δεξιά. Η scanf λειτουργεί προσπαθώντας να ταιριάξει αυτό που προσδιορίσαµε στο format string µε αυτό που βλέπει να έρχεται από το πληκτρολόγιο. Εφόσον εµείς πρόκειται να γράψουµε στο πληκτρολόγιο έναν ακέραιο, ένα κόµµα, έναν ακέραιο, µια παύλα και ξανά έναν ακέραιο, ένα κόµµα, έναν ακέραιο, αυτό ακριβώς πρέπει να προσδιορίσουµε και στο format string: scanf("%d,%d-%d,%d", &new_sprite.left_x, &new_sprite.left_y, &new_sprite.right_x, &new_sprite.right_y); Ελέγχει αν υπάρχει άλλο sprite που καταλαµβάνει τον ίδιο χώρο. Αν ναι, τότε το νέο sprite ΔΕΝ εισάγεται, αλλά κάθε ήδη υπάρχον που επικαλύπτεται απο το νέο ΚΑΙ έχει χαµηλότερη ζωή από το νέο, χάνει ένα πόντο ζωής (εφόσον αυτή είναι θετική) και το πρόγραµµα εκτυπώνει το µήνυµα "A" beat "B" (L) ακολουθούµενο από χαρακτήρα αλλαγής γραµµής. Α είναι το όνοµα του νέου sprite, Β του ήδη υπάρχοντος και L η ζωή του B. Μην ξεχάσετε τα " " γύρω από το κάθε όνοµα. Αν όχι, τότε το νέο sprite εισάγεται στην επόµενη διαθέσιµη θέση του πίνακα και το πρόγραµµα εκτυπώνει το µήνυµα Added "A" (L) ακολουθούµενο από χαρακτήρα αλλαγής γραµµής όπου Α το όνοµα του νέου sprite και L η ζωή του.
Εδώ είναι το αλγοριθµικό κοµµάτι της άσκησης: Έχουµε ένα νέο sprite. Το αν θα καταχωρηθεί στον πίνακα η όχι, εξαρτάται από το αν υπάρχουν επικαλύψεις µε άλλα sprites. Άρα σίγουρα πρέπει να ελέγξουµε την επικάλυψη του µε τα υφιστάµενα sprites που είναι αποθηκευµένα στον πίνακα Ένα συχνό λάθος που γινόταν εδώ ήταν να πηγαίνει το loop µέχρι MAX_SLOTS. Αυτό δεν έχει νόηµα και θα βγάλει λάθος αποτελέσµατα γιατί οι θέσεις του πίνακα από το num_slots µέχρι το MAX_SLOTS περιέχουν σκουπίδια. Για να δούµε τώρα τι πάει µέσα στο loop. Γνωρίζουµε ότι αν υπάρχει έστω και µια επικάλυψη µε άλλο sprite, δε µπορεί να γίνει η κράτηση, και δε χρειάζεται να συνεχίσουµε τον έλεγχο. Η συνθήκη ελέγχου είναι η άρνηση της συνθήκης ελέγχου που περιγράφεται στην τελευταία σελίδα της εκφώνισης. Άρα: if(!(new_sprite.left_x > sprites[i].right_x sprites[i].left_x > new_sprite.right_x new_sprite.left_y < sprites[i].right_y sprites[i].left_y < new_sprite.right_y) ) { /* μείωση της ζωής κατά 1 ΜΟΝΟ για τα sprites με χαμηλότερη ζωή */ if( new_sprite.life > sprites[i].life) { sprites[i].life -; printf("\ %s\ beat \ %s\ (%d)", new_sprite.name, sprites[i].name, sprites[i].life); /* εκτύπωση µηνυµατος για overlap */ break; /* γιατί δεν χρειάζεται να κάνουµε άλλους ελέγχους */ Αυτό τακτοποιεί την περίπτωση που ανιχνεύουµε επικάλυψη. Να δούµε τώρα την περίπτωση που δεν υπάρχει. Για να ξέρουµε στα σίγουρα ότι δεν υπαρχει καµία επικάλυψη, πρέπει να έχουµε ελέγξει ΟΛΕΣ τις ήδη υπάρχουσες καταχωρήσεις και να µην έχουµε βρει κάτι. Με άλλα λόγια, πρέπει να έχει τελειώσει η εκτέλεση του for και να έχουµε βγει κανονικά (όχι από το break). Πώς µπορούµε να ξεχωρίσουµε αν το loop τερµάτισε πρόωρα (λόγω break) ή κανονικά (λόγω συνθήκης)? Αν έχει βγει λόγω break τότε το i θα είναι σίγουρα µικρότερο του num_sprites. Αν έχει βγει λόγω συνθήκης, τότε βγήκε ακριβώς τη στιγµή που η συνθήκη έγινε ψευδής, δηλαδή όταν το i έγινε ίσο µε num_reserved. Αν λοιπόν συνέβη αυτό, αποθηκεύουµε στον πίνακα το νέο sprite, εκτυπώνουµε το µήνυµα και αυξάνουµε τον αριθµό των κρατήσεων. Παρατηρήστε πώς εφόσον έχουµε ακριβώς num_sprites κρατήσεις, αυτές βρίσκονται στις θέσεις 0 έως και num_sprites-1 του πίνακα, άρα η νέα κράτηση θα µπει στη θέση num_sprites. if (i == num_sprites) { sprite[num_sprites] = new_sprite; num_sprites++; printf("added \ %s\ (%d)\n", new_sprite.name, new_sprite.life);
Ένα πολύ συχνό ΛΑΘΟΣ εδώ ήταν το παρακάτω: if (/* επικάλυψη */) { printf("..."); /* εκτύπωση µηνυµατος για overlap */ break; else { /* όχι επικάλυψη */ slots[num_sprites] = new_sprite; num_sprites++; printf("...\n"); Σε αυτό τον κώδικα παίρνουµε τελική απόφαση για το αν πρέπει να γίνει καταχώρηση του νέου sprite η όχι από το ΠΡΩΤΟ στοιχείο που ελέγχουµε. Πιθανόν ελέγχοντας το πρώτο sprite να µην υπάρχει επικάλυψη, αλλά να υπάρχει στο δεύτερο ή στο τρίτο κ.ο.κ. πράγµα που ο παραπάνω κώδικας αγνοεί. Καλεί µια συνάρτηση η οποία παίρνει ως παραµέτρους τον πίνακα κρατήσεων και το πλήθος κρατήσεων κι εκτυπώνει τα περιεχόµενά του (δείτε πώς παρακάτω). Ας αφήσουµε προς το παρόν τη συνάρτηση για µετά... Εκτυπώνει το µήνυµα More (y/n)? και διαβάζει ένα χαρακτήρα. Αν αυτός είναι y ή Y, συνεχίζει η επανάληψη, αν είναι n ή N τερµατίζει η είσοδος δεδοµένων, ενώ σε κάθε άλλη περίπτωση επαναλαµβάνει αυτό το βήµα. Εδώ θέλουµε µια µικρή επανάληψη που να διαβάζει ένα χαρακτήρα από το χρήστη, και βεβαιώνει ότι είναι ένας εν των n, N, y, N. Για να µην έχουµε τεράστια συνθήκη, µπορούµε να µετατρέπουµε πάντα το χαρακτήρα σε µικρό αφού τον διαβάσουµε, ώστε να ελέγχουµε µόνο για n, y: scanf(" %c", &go_on); go_on = tolower(go_on); while (go_on!= 'y' && go_on!= 'n'); Συχνά λάθη εδώ: Λάθος 1: Πολλοί ξέχναγαν το κενό πριν το %c, παρόλο που είναι κάτι που έχει εξηγηθεί σε αρκετά προηγούµενα εργαστήρια! Λάθος 2: Χρήση αντί για && στη συνθήκη, µε αποτέλεσµα να µη λειτουργεί σωστά. Αν βάλουµε και το go_on είναι για παράδειγµα 'y', τότε το go_on!= 'n' θα βγει αληθές και η επανάληψη θα συνεχίσει, ενώ δεν πρέπει! Εναλλακτικά, µπορείτε να γράψετε scanf(" %c", &go_on); go_on = tolower(go_on); while (!(go_on == 'y' go_on == 'n')); Εδώ επανερχόµαστε στη συνθήκη τερµατισµού του εξωτερικού while. Το loop συνεχίζει να εκτελείται εφόσον το go_on είναι y. Μπορούµε είτε να προσθέσουµε µια συνθήκη if σε αυτό το σηµείο που να κάνει break αν το go_on είναι 'n' είτε να προσθέσουµε στην αρχική συνθήκη του while το && ( go_on == 'y') δηλ. while (num_sprites < MAX_SLOTS && ( go_on == 'y');.
Γράψτε µια συνάρτηση η οποία παίρνει ως παραµέτρους τον πίνακα από sprites και το πλήθος τους κι εκτυπώνει χαρακτήρα αλλαγής γραµµής, µετά τα έγκυρα περιεχόµενα του πίνακα όπως περιγράφουµε πιο κάτω, και τέλος πάλι χαρακτήρα αλλαγής γραµµής: Για κάθε χαρακτήρα παιχνιδιού εκτυπώνει το µήνυµα N: L (LX,LY)-(RX,RY) ακολουθούµενο από χαρακτήρα αλλαγής γραµµής, όπου N το όνοµα του χαρακτήρα, L οι πόντοι ζωής του, και LX,LY κι RX,RY οι συντεταγµένες της πάνω αριστερά και κάτω δεξιά γωνίας αντίστοιχα. Δεδοµένης της παραπάνω περιγραφής, το prototype της συνάρτησης θα είναι: void print_sprites(struct spritet s[], int size) { int i; printf("\n"); for(i=0; i<size; i++) { printf("%s: %d (%d,%d)-(%d,%d)\n", s[i].name, s[i].life, s[i].left_x, s[i].left_y, s[i].right_x, s[i].right_y); printf("\n"); Ολοκληρώνουµε την υλοποίησή της, η οποία διατρέχει τον πίνακα µέχρι το num_reserved κι εκτυπώνει τα στοιχεία των κρατήσεων, και την καλούµε στη main, στο σηµείο που ζητά η εκφώνηση: print_reservations(slots, num_sprites); Συχνά λάθη εδώ ήταν παντελής άγνοια του πώς περνάµε πίνακα ως παράµετρο συνάρτησης (παρόλο που το κάναµε στο lab10) και κλήση της συνάρτησης σε λάθος σηµείο: πρέπει να εκτελείται σε κάθε επανάληψη, ακριβώς πριν εκτυπωθεί το µήνυµα More (y/n)? και όχι µόνο µια φορά στο τέλος του προγράµµατος. Παρατηρήσεις: Χρησιµοποιήστε 100 για NAME_SIZE και 10 για MAX_SLOTS.