Videogames Laboratory

O συναρπαστικός κόσμος της ανάπτυξης βιντεοπαιχνιδιών

Arkanoid: Σχεδιασμός πίστας

Posted by Kostas Anagnostou στο 7 Δεκεμβρίου, 2009

Κοιτάζοντας το πότε ανέβασα το τελευταίο άρθρο στο blog διαπιστώνω ότι έχει περάσει πάνω από ένας μήνας! Ακόμα περισσότερο έχουμε να ασχοληθούμε με το Arkanoid. Ήταν δύσκολες οι προηγούμενες εβδομάδες στο Πανεπιστήμιο με τις προθεσμίες για προτάσεις χρηματοδότησης και άρθρα σε περιοδικά να διαδέχονται η μία την άλλη. Και ενώ είχα γράψει το κώδικα για το σχεδιασμό πίστας στο Arkanoid δεν έβρισκα το χρόνο να γράψω το κείμενο. Τώρα όμως χαλάρωσαν λίγο τα πράγματα και επανερχόμαστε στην ενεργό δράση!

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

Μέχρι στιγμής στο Arkanoid χρησιμοποιούμε το παρακάτω κώδικα για να σχεδιάσουμε τη πίστα και να τοποθετήσουμε τα τουβλάκια στην οθόνη:

for (int j = 0; j < numOfRows; j++)
{
    for (int i = 0; i < bricksPerRow; i++)
    {
        Rectangle rect = new Rectangle(i * (brickWidth + brickSpacing),
                                       rowStart + j * (brickHeight + brickSpacing),
                                       brickWidth, brickHeight);
        bricks[j * bricksPerRow + i] = new Brick(rect, Color.Red, whiteTile);
        numOfVisibleBricks++;
    }
}

Με ένα for-loop δηλαδή τοποθετούμε τα τουβλάκια, ανά τακτά διαστήματα, σε γραμμές και στήλες. Αυτός το τρόπος σχεδιασμού πίστας (level design) είναι πολύ δύσχρηστος και επιβάλλει στο σχεδιαστή της πίστας να συνεργάζεται άμεσα με το προγραμματιστή έτσι ώστε να του υποδεικνύει τι αλλαγές πρέπει να γίνουν στο σχέδιο της πίστας και πού. Επίσης κάθε αλλαγή στο σχεδιασμό της πίστας συνεπάγεται compile του κώδικα του παιχνιδιού, μια χρονοβόρα διαδικασία. Επιπλέον είναι αρκετά δύσκολο να αλλάξεις το σχέδιο της πίστας σε κάτι άλλο από γραμμές με τουβλάκια. Από την άλλη είναι επιθυμητό ο σχεδιαστής να μην ασχολείται καθόλου με το κώδικα του παιχνιδιού, να σχεδιάζει με κάποιο τρόπο τη πίστα και να μεταφέρει το αποτέλεσμα στο προγραμματιστή για απεικόνιση. Η σχεδίαση δηλαδή να γίνεται με βάση τα δεδομένα (πχ αρχείο θέσεων) και όχι το κώδικα (data-driven development), όπως γίνεται άλλωστε και στη βιομηχανία ανάπτυξης βιντεοπαιχνιδιών.

Στο 2ο Videogames Laboratory Challenge είχα ζητήσει από τους αναγνώστες του blog να προτείνουν βελτιώσεις στο τρόπο αυτό σχεδιασμού της πίστας. Προτάθηκαν διάφορες λύσεις, από εύκολες, όπως να διαβάζουμε τις θέσεις των τούβλων από αρχείο μέχρι πιο πολύπλοκες, όπως ένα εξειδικευμένο πρόγραμμα που να επιτρέπει το σχεδιασμό της πίστας με οπτικό τρόπο (visual editor). Στα πλαίσια του άρθρου θα υλοποιήσουμε την απλούστερη λύση, δηλαδή να διαβάζει το παιχνίδι τη θέση των τούβλων από αρχείο. Στο μέλλον θα κάνουμε μια παρουσίαση πιο ολοκληρωμένων προτάσεων που βασίζονται σε editor.

Δεν είναι πολλές οι αλλαγές που πρέπει να κάνουμε στον υπάρχοντα κώδικα για να υλοποιήσουμε τη λειτουργία αυτή. Καταρχάς πρέπει να καθορίσουμε τη μορφή και το περιεχόμενο του αρχείου κειμένου που θα περιέχει το σχέδιο της πίστας. Κάνουμε τις εξής παραδοχές:

  • Μια γραμμή κειμένου στο αρχείο θα αντιστοιχεί σε μια γραμμή τούβλων στην οθόνη
  • Οι γραμμές τούβλων στην οθόνη αρχίζουν από το πάνω μέρος (Υ=0)
  • Χρησιμοποιούμε τους χαρακτήρες R, G, B, Y, W για να δηλώσουμε τη θέση τούβλων χρώματος κόκκινο, πράσινο, μπλε, κίτρινο και άσπρο
  • Ο χαρακτήρας ‘.’ υποδηλώνει κενό τούβλο
  • Οι γραμμές κειμένου στο αρχείο πρέπει να έχουν όλες το ίδιο μέγεθος

Με βάση τις παραδοχές αυτές το αρχείο που περιγράφει μια πίστα στο παιχνίδι θα μπορούσε να είναι:

………….
………….
..RRRRRRRRR..
..GGGGGGGGG..
….BBBBB….
..BYBBBBBYB..
..YY…..YY..
..GGGGGGGGG..

Θα αφήσουμε ελεύθερο τον αριθμό των τούβλων σε μια γραμμή (επιλογή σχεδιαστή) και θα προσαρμόζουμε το πλάτος του κάθε τούβλου ανάλογα. Με κάποιο πρόγραμμα επεξεργασίας κειμένου (καλύτερη επιλογή μάλλον το Notepad), φτιάχνουμε ένα αρχείο με το όνομα Level01.txt και σχεδιάζουμε τη πίστα με τις παραπάνω προδιαγραφές. Μπορείτε να κάνετε copy-paste το παραπάνω σχέδιο επίσης. Προσοχή, το αρχείο πρέπει να είναι απλό ASCII και όχι κάποιο άλλο format.

Στην συνέχεια, στο Content project του Arkanoid, επιλέγουμε δεξί-κλικ Add/Existing Item και επιλέγουμε το Level01.txt. Όταν το αρχείο εμφανιστεί στη λίστα του Content κάνουμε δεξί κλικ και επιλέγουμε Properties από το μενού. Στις ιδιότητες του αρχείου αυτού επιλέγουμε Build Action: None και Copy to Output: Copy if newer.


Ο λόγος που το κάνουμε αυτό είναι γιατί δεν θέλουμε να επεξεργαστεί το αρχείο αυτό το Content Pipeline του XNA, το οποίο θα του αλλάξει μορφοποίηση και θα συμπιέσει το περιεχόμενο του. Το θέλουμε ως απλό αρχείο κειμένου για να το διαβάσουμε κατά την αρχικοποίηση του παιχνιδιού. Είμαστε έτοιμη να φορτώσουμε το αρχείο με το σχέδιο πίστας.

Θα κάνουμε αρχικά μερικές μικρές αλλαγές στο κώδικα. Καταρχάς θα χρησιμοποιήσουμε ένα List για να αποθηκεύσουμε στη μνήμη τα τουβλάκια και όχι ένα πίνακα. Οι διαφορές μεταξύ των δύο είναι μικρές όσον αφορά τη λειτουργικότητα. Στην περίπτωση μας βολεύει η List γιατί δεν απαιτεί να ορίσω το μέγεθος της εκ των προτέρων όπως πρέπει σε ένα πίνακα. Μπορεί να δεσμεύσει περισσότερη μνήμη μόνη της αν χρειαστεί σύμφωνα με τον αριθμό των τούβλων που ορίζει το αρχείο.

    public class Game1 : Microsoft.Xna.Framework.Game
    {
//υπόλοιπος κωδικας
        List<string> lines;
        List<Brick> bricks;

Στην αρχή της κλάσης Game1 ορίζω 2 μεταβλητές List μια για τις γραμμές κειμένου του αρχείου (lines) και μια για τα τουβλάκια που θα αποθηκεύσω στη μνήμη. Στην Initialize(), δημιουργώ τα αντίστοιχα αντικείμενα List για τα lines και bricks. Οι λίστες είναι κενές σε αυτό το σημείο.

        protected override void Initialize()
        {
		//υπόλοιπος κωδικας
            lines = new List<string>();
            bricks = new List<Brick>();

            base.Initialize();
        }

Το αρχείο θα το διαβάσουμε στην LoadContent, όπως όλα τα αρχεία περιεχομένου (τεχνικά δεν χρειάζεται εδώ αλλά είναι καλή πρακτική).

        protected override void LoadContent()
        {
		//υπόλοιπος κωδικας

            string path = Path.Combine(StorageContainer.TitleLocation, "Content/Level01.txt");

            StreamReader reader = new StreamReader(path);
            string line = reader.ReadLine();
            while (line != null)
            {
                lines.Add(line);
                line = reader.ReadLine();
            }
            reader.Close();
        }

Καταρχάς συνθέτουμε τη διαδρομή που βρίσκεται το αρχείο level01.txt. Αυτό το κάνουμε προσθέτοντας το όνομα του αρχείου «Content/Level01.txt» στην εξορισμού τοποθεσία στην οποία αποθηκεύει το περιεχόμενο το XNA, StorageContainer.TitleLocation. Το αποτέλεσμα το κρατάμε σε μια μεταβλητή path.

Για να φορτώσουμε ένα αρχείο με τη χρήση C#/.NET θα χρησιμοποιήσουμε ένα αντικείμενο reader τύπου StreamReader το οποίο μπορεί να διαβάσει χαρακτήρες κειμένου. Το StreamReader παίρνει ως όρισμα το (πλήρες) όνομα του αρχείου κειμένου. Στην συνέχεια διαβάζουμε το αρχείο γραμμή-γραμμή (reader.ReadLine()) και προσθέτουμε τη κάθε γραμμή στη λίστα lines. Όταν διαβάσουμε όλο το αρχείο (line==null), τότε κλείνουμε το reader καλώντας τη μέθοδο του Close(), για να αποδεσμεύσουμε τους πόρους του συστήματος. Κατά την ανάγνωση του αρχείου δεν κάνουμε καμιά προσπάθεια να εξασφαλίσουμε το ότι ο αριθμός τούβλων είναι ίδιος σε κάθε γραμμή, χάριν ευκολίας, είναι όμως κάτι το οποίο θα πρέπει να έχουμε υπόψη μας.

Για να ολοκληρώσουμε το κώδικα πρέπει να αξιοποιήσουμε τη λίστα lines για να τοποθετήσουμε τα τουβλάκια στην οθόνη. Αυτό γίνεται στην resetGame().

        private void resetGame()
        {
            paddle = new Rectangle(150, 550, 90, 15);
            paddleSpeed = 10;

            ball = new Rectangle(150, 400, 15, 15);
            ballDirection = new Vector2(1, -1);
            ballDirection.Normalize();
            ballSpeed = 5;

            lives = 3;
            score = 0;

            bricksPerRow = lines[0].Length;
            brickWidth = (viewWidth - (bricksPerRow - 1) * brickSpacing) / bricksPerRow;

            bricks.Clear();

            for (int j = 0; j < lines.Count; j++)
            {
                for (int i = 0; i < lines[j].Length; i++)
                {
                    Rectangle rect = new Rectangle(i * (brickWidth + brickSpacing),
                                                   j * (brickHeight + brickSpacing),
                                                   brickWidth, brickHeight);
                    switch (lines[j][i])
                    {
                        case 'R':
                            bricks.Add(new Brick(rect, Color.Red, whiteTile));
                            break;
                        case 'G':
                            bricks.Add(new Brick(rect, Color.Green, whiteTile));
                            break;
                        case 'B':
                            bricks.Add(new Brick(rect, Color.Blue, whiteTile));
                            break;
                        case 'Y':
                            bricks.Add(new Brick(rect, Color.Yellow, whiteTile));
                            break;
                        case 'W':
                            bricks.Add(new Brick(rect, Color.White, whiteTile));
                            break;

                    }
                }
            }
            numOfVisibleBricks = bricks.Count;
        }

Ο αριθμός των τούβλων ανά γραμμή καθορίζεται από το μήκος της πρώτης γραμμής κειμένου της λίστας list (list[0].Length). Το πλάτος κάθε τούβλου καθορίζεται τώρα με βάση αυτό. Στην συνέχεια, σε ένα διπλό for-loop, διατρέχουμε όλες τις γραμμές τις λίστας list και για κάθε γραμμή εξετάζουμε το κάθε χαρακτήρα της με ένα switch-statement. Αν ο χαρακτήρας είναι κάποιος από τους R, G, B, Y, W, τότε προσθέτουμε ένα τουβλάκι αντίστοιχου χρώματος στη λίστα bricks. Στην ουσία το αρχείο κειμένου με το σχέδιο της πίστας ουσιαστικά δημιουργεί ένα πλέγμα (grid), σε κάθε κελί του οποίου μπορεί να υπάρχει ή όχι ένα τουβλάκι. Επιπλέον το γεγονός ότι δημιουργούμε ένα Rectangle για κάθε θέση του πλέγματος, ανεξάρτητα από το αν αυτό περιέχει τουβλάκι ή όχι δεν είναι και πολύ αποδοτικό. Ο αριθμός numOfVisibleBricks τον οποίο χρησιμοποιούμε για να διαπιστώσουμε αν ο παίκτης έχει χτυπήσει όλα τα τουβλάκια στην οθόνη (μειώνοντας τον κατά ένα με κάθε σύγκρουση) είναι τώρα ίσος με τον αριθμό των τούβλων στη λίστα bricks. Τέλος μια παρατήρηση, ο χαρακτήρας ‘.’ που θέσαμε ως κενό χαρακτήρα δεν χρησιμοποιείται στην πραγματικότητα είναι περισσότερο «οπτική διευκόλυνση» για εμάς που σχεδιάζουμε τη πίστα.

Τρέχοντας το παιχνίδι βλέπουμε τη νέα πίστα στην οθόνη μας όπως τη σχεδιάσαμε.


Ο κώδικας του παιχνιδιού είναι διαθέσιμος μέσω SVN από το Code Repository καθώς και σε zip μορφή.

Στο επόμενο και τελευταίο άρθρο στη σειρά ανάπτυξης του Arkanoid θα προσθέσουμε τη δυνατότητα για διαφορετικά επίπεδα (πίστες) στο παιχνίδι.

Μέχρι τότε μπορείτε να ανεβάσετε τα δικά σας ευφάνταστα σχέδια πίστας του Arkanoid στο forum. Τα πιο πρωτότυπα θα χρησιμοποιηθούν ως πίστες στη τελική έκδοση του Arkanoid που θα δημιουργήσουμε στο επόμενο tutorial.

7 Σχόλια to “Arkanoid: Σχεδιασμός πίστας”

  1. darklynx said

    Ωραιά!Θα μπορούσαμε επίσης να επεκτείνουμε την Content Pipeline ώστε να κάνει parse τα αρχεία με τα επίπεδα κατά το compile και να τα προσθέτει στο content του παιχνιδιού.Θα γλυτώναμε έτσι το parsing κατά το φόρτωμα του παιχνιδιού και θα διευκολύναμε τη διανομή του παιχνιδιού,μια που το επίπεδο θα ήταν ένα κανονικό asset στα .xnb αρχεία και όχι ένα ξεχωριστό .txt.Μάλιστα η διαδικασία αυτή δεν είναι ιδιαίτερα δύσκολη.

  2. Σωστό! Θα μπορούσε η λίστα bricks να είναι asset εξαρχής και να την διαβάζουμε έτοιμη από το δίσκο. Όμως θέλει εξήγηση για το πως λειτουργεί το content pipeline για το κάνουμε αυτό. Θα γίνει και αυτό…

  3. konsnos said

    Ωραίο! Απλό και λειτουργικό.
    Δεν κατάλαβα όμως πως ακριβώς λειτουργεί ο κώδικας Path.Combine(StorageContainer.TitleLocation, «Content/Level01.txt»);. Έχει κάποια σχέση με absolute και relative paths στο άνοιγμα των αρχείων;

  4. Ναι, έχει κάποια σχέση. To StorageContainer.TitleLocation είναι η absolute τοποθεσία (folder) από την οποία τρέχει το παιχνίδι. Αναμενόμενα ο folder αυτός θα είναι διαφορετικός για κάθε πλατφόρμα (Windows, Xbox 360 και Zune), οπότε το XNA αναλαμβάνει να μας ενημερώσει αυτόματα για την τοποθεσία αυτή. Από εκεί και πέρα η Path.Combine ενώνει το StorageContainer.TitleLocation με τη relative ονομασία του αρχείου (Content/level1.txt) για να πάρουμε τελικά την απόλυτη θέση του αρχείου.

  5. […] και απώλειας ζωής του παίκτη, game entities, game state management. Στο προηγούμενο tutorial είδαμε πως μπορούμε να δημιουργήσουμε μια πίστα του […]

  6. […] Κοιτάζοντας το πότε ανέβασα το τελευταίο άρθρο στο blog διαπιστώνω ότι έχει περάσει πάνω από ένας μήνας! Ακόμα περισσότερο έχουμε να ασχοληθούμε με το Arkanoid. Ήταν δύσκολες οι προηγούμενες εβδομάδες στο Πανεπιστήμιο με τις προθεσμίες για προτάσεις χρηματοδότησης και άρθρα σε περιοδικά να διαδέχονται η μία την άλλη. [Περισσότερα] […]

  7. […] Permanent link to Arkanoid- Σχεδιασμός πίστας […]

Sorry, the comment form is closed at this time.

 
Αρέσει σε %d bloggers: