Videogames Laboratory

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

Hello Pong: μέρος 4ο

Δημοσιεύθηκε από Κώστας Αναγνώστου στο Ιουλίου 17, 2009

Το ενδιαφέρον για την σειρά άρθρων πάνω στην ανάπτυξη ενός παιχνιδιού με τη χρήση του XNA Game Studio πέφτει με εκθετικό ρυθμό! Ελπίζω να οφείλονται οι διακοπές του καλοκαιριού για αυτό και όχι τα άρθρα τα ίδια! J Πάντως λαμβάνω πολύ εύστοχα και κριτικά σχόλια στα άρθρα και αυτό με ευχαριστεί πολύ.

Σήμερα θα ολοκληρώσουμε επιτέλους το Pong στην βασική του μορφή, εισάγοντας κίνηση του αντιπάλου, σκορ και ηχητικά εφέ. Το σχόλιο του Σπύρου (spahar) στο προηγούμενο άρθρο με δελέασε να αλλάξω τον τρόπο με τον οποίο συγκρούεται η μπάλα στη ρακέτα ώστε να είναι πιο ρεαλιστικός και το παιχνίδι πιο ενδιαφέρον, αλλά τελικά αποφάσισα να το κάνουμε αυτό μετά την ολοκλήρωση του παιχνιδιού στην απλή του μορφή ώστε να μην περιπλέξουμε τα πράγματα.

Η τεχνητή νοημοσύνη (artificial intelligence) του αντιπάλου που θα υλοποιήσουμε θα είναι παραπάνω από απλή. Το μόνο που θα κάνει είναι να ακολουθεί την μπάλα στην προσπάθεια του να την αποκρούσει. Σε κώδικα αυτό μεταφράζεται ως εξής:

protected override void Update(GameTime gameTime)
{
// υπόλοιπος κώδικας

	//κίνηση μπάλας
	ball.X = (int)(ball.X + ballSpeed * ballDirection.X);
	ball.Y = (int)(ball.Y + ballSpeed * ballDirection.Y);

	//κινηση ρακέτας υπολογιστή
	int sign = Math.Sign((ball.Top + ball.Height / 2) - (rightPaddle.Top + rightPaddle.Height / 2));
	rightPaddle.Y += (int)(sign*rightPaddleSpeed);

	if (rightPaddle.Bottom > viewHeight)
	{
		rightPaddle.Y = viewHeight - rightPaddle.Height;
	}
	else if (rightPaddle.Y < 0)
	{
		rightPaddle.Y = 0;
	}            

// υπόλοιπος κώδικας
	base.Update(gameTime);
}

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


Θυμηθείτε ότι στο σύστημα συντεταγμένων παραθύρου η αρχή (0,0) είναι πάνω-αριστερά το Υ αυξάνει προς τα κάτω και το Χ προς τα δεξιά. Η απόσταση Α είναι η διαφορά της Υμ συντεταγμένης του κέντρου της μπάλας με τη Υρ συντεταγμένη του κέντρου της ρακέτας. Αρνητική απόσταση σημαίνει ότι πρέπει να μετακινήσω τη ρακέτα προς τα πάνω για να αποκρούσω τη μπάλα και θετική το ανάποδο.

Το πρόσημο που μας ενδιαφέρει μπορούμε να το υπολογίσουμε με την συνάρτηση Math.Sign(). Αυτή επιστρέφει 1 ή -1 ανάλογα με το αν η παράμετρος που τις δίνουμε είναι θετική η αρνητική. Επιπλέον έχω ορίσει και μια float rightPaddleSpeed μεταβλητή την οποία έχω αρχικοποιήσει στη μέθοδο Initialise() με τιμή 5.0f. Η μεταβλητή αυτή κρατά την απόσταση που θα μετακινώ την ρακέτα σε κάθε καρέ. Έχοντας υπολογίσει το πρόσημο της απόστασης και έχοντας το αποθηκευμένο στη μεταβλητή sign, μπορώ να μετακινήσω τη ρακέτα του υπολογιστή πολύ απλά με την εντολή:

int sign = Math.Sign((ball.Top + ball.Height / 2) - (rightPaddle.Top + rightPaddle.Height / 2));
rightPaddle.Y += (int)(sign*rightPaddleSpeed);

Το sign μου καθορίζει αν θα πρέπει να προσθέσω ή να αφαιρέσω τη ποσότητα rightPaddleSpeed από τη Υ συνταγμένη της ρακέτας. Όλη αυτή η πολύπλοκη εξήγηση μεταφράστηκε σε 2 γραμμές κώδικα! Η συνθήκη if που ακολουθεί δεν κάνει τίποτα άλλο από να βεβαιώνει ότι η ρακέτα του υπολογιστή δεν θα βγει ποτέ εκτός οθόνης, κάτι που κάναμε και για την ρακέτα του παίκτη στο προηγούμενο άρθρο.

	if (rightPaddle.Bottom > viewHeight)
	{
		rightPaddle.Y = viewHeight - rightPaddle.Height;
	}
	else if (rightPaddle.Y < 0)
	{
		rightPaddle.Y = 0;
	}

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

protected override void Update(GameTime gameTime)
{
// υπόλοιπος κώδικας

	//ανίχνευση σύγκρουσης μπάλας με τη ρακέτα υπολογιστή
	if (rightPaddle.Top <= ball.Top && ball.Bottom <= rightPaddle.Bottom)
	{
		if (ball.Right > rightPaddle.Left && ball.Left < rightPaddle.Left && ballDirection.X > 0)
		{
			ballDirection.X = -ballDirection.X;
		}
	}

	base.Update(gameTime);
}

Η λογική είναι εντελώς ανάλογη με αυτή που χρησιμοποιήσαμε στο προηγούμενο άρθρο για τη ρακέτα του παίκτη, και επιπλέον ενσωματώνει μια έξτρα συνθήκη για να κάνει την ανίχνευση σύγκρουσης περισσότερο αξιόπιστη, μετά την πολύ εύστοχη παρατήρηση του spahar.

Το παιχνίδι μας είναι λειτουργικό πλέον και μπορείτε να το δοκιμάσετε. Για να το κάνουμε πιο ενδιαφέρον ας προσθέσουμε και σκορ. Θέλουμε 2 μεταβλητές για το σκορ, μια για κάθε παίκτη (playerScore και computerScore).

public class Game1 : Microsoft.Xna.Framework.Game
{
	GraphicsDeviceManager graphics;
	SpriteBatch spriteBatch;
	Rectangle rightPaddle;
	Rectangle leftPaddle;
	float rightPaddleSpeed;
	int playerScore;
	int computerScore;

  // υπόλοιπος κώδικας

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

protected override void Update(GameTime gameTime)
{
   // υπόλοιπος κώδικας

	//ανίχνευση σύγκρουσης μπάλας με τα τοιχώματα του "γηπέδου"
	if (ball.Left < 0)
	{
		ball.X = 0;
		ballDirection.X = -ballDirection.X;
		computerScore++;
	}
	else if (ball.Right > viewWidth)
	{
		ball.X = viewWidth - ball.Width ;
		ballDirection.X = -ballDirection.X;
		playerScore++;
	}

   // υπόλοιπος κώδικας
}

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

Μπορούμε να κρατήσουμε το σκορ τώρα, αλλά δεν μπορούμε να το απεικονίσουμε στην οθόνη ακόμα. Για να απεικονίσουμε κείμενο στο XNA πρέπει πρώτα να προσθέσουμε ένα νέο αρχείο (New Item) περιεχομένου στο project Content:


Από το παράθυρο που ανοίγει επιλέγουμε Sprite Font, του δίνουμε το όνομα Arial.Spritefont και πατάμε το Add.


Στο Content project θα υπάρχει τώρα ένα XML αρχείο με το όνομα Arial.spritefont. Ανοίξτε το και θα δείτε τη περιγραφή ενός font:

<?xml version="1.0" encoding="utf-8"?>
<!--
This file contains an xml description of a font, and will be read by the XNA
Framework Content Pipeline. Follow the comments to customize the appearance
of the font in your game, and to change the characters which are available to draw
with.
-->
<XnaContent xmlns:Graphics="Microsoft.Xna.Framework.Content.Pipeline.Graphics">
  <Asset Type="Graphics:FontDescription">

    <!--
    Modify this string to change the font that will be imported.
    -->
    <FontName>Kootenay</FontName>

    <!--
    Size is a float value, measured in points. Modify this value to change
    the size of the font.
    -->
    <Size>14</Size>

Αυτό που μας ενδιαφέρει είναι η μεταβλητή FontName την οποία θα αλλάξουμε σε Arial από Kootenay (που το βρήκε αυτό το font;), και τη Size που θα την αυξήσουμε σε 60.

Η περιγραφή του font είναι έτοιμη αλλά δεν μπορούμε να το χρησιμοποιήσουμε ακόμα, πρέπει πρώτα να το φορτώσουμε στη μνήμη. Για να το κάνουμε αυτό πρέπει να δημιουργήσουμε ένα αντικείμενο τύπου SpriteFont, το οποίο περιγράφει font στο περιβάλλον XNA. Αρχικά δηλώνουμε μια μεταβλητή arialFont τύπου SpriteFont:

public class Game1 : Microsoft.Xna.Framework.Game
{
	GraphicsDeviceManager graphics;
	SpriteBatch spriteBatch;
	Rectangle rightPaddle;
	Rectangle leftPaddle;
	float rightPaddleSpeed;
	int playerScore;
	int computerScore;
	SpriteFont arialFont;

	// υπόλοιπος κώδικας

Στη μέθοδο LoadContent φορτώνουμε το font προσθέτοντας μια Load εντολή:

protected override void LoadContent()
{
	// Create a new SpriteBatch, which can be used to draw textures.
	spriteBatch = new SpriteBatch(GraphicsDevice);

	whiteTile = Content.Load<Texture2D>("white_tile");
	arialFont = Content.Load<SpriteFont>("arial");
}

Παρατηρούμε ότι χρησιμοποιούμε και πάλι την μέθοδο Content.Load όπως και με την υφή. Αυτή τη φορά όμως ως τύπο περιεχομένου δίνουμε SpriteFont. Και πάλι δίνουμε ως όρισμα στην Load το όνομα του αρχείου περιεχομένου, χωρίς την κατάληξη (.spritefont). Το XNA δεν διαχωρίζει κεφαλαία-μικρά. Το font μας είναι πλέον έτοιμο να το χρησιμοποιήσουμε. Αυτό θα το κάνουμε στη μέθοδο Draw().

Το αντικείμενο τύπου SpriteBatch, που χρησιμοποιούμε ήδη για να απεικονίσουμε ρακέτες και μπάλα, παρέχει μια μέθοδο ειδική για απεικόνιση κειμένου, τη DrawString(). Η μέθοδος αυτή παίρνει ως ορίσματα το αντικείμενο arialFont που δημιουργήσαμε παραπάνω, το κείμενο που θα τυπώσει, την τοποθεσία και το χρώμα του κειμένου. Συνεπώς πάντα ανάμεσα στο spriteBatch.Begin()/.End() καλώ τη μέθοδο αυτή 2 φορές για να απεικονίσω το σκορ κάθε παίκτη:

protected override void Draw(GameTime gameTime)
{
	GraphicsDevice.Clear(Color.Black);

	spriteBatch.Begin();

	//απεικόνισε ρακέτες και μπάλα
	spriteBatch.Draw(whiteTile, rightPaddle, Color.White);
	spriteBatch.Draw(whiteTile, leftPaddle, Color.White);
	spriteBatch.Draw(whiteTile, ball, Color.White);

	//απεικόνισε σκορ
	spriteBatch.DrawString(arialFont, playerScore.ToString(), new Vector2(220, 10), Color.Yellow);
	spriteBatch.DrawString(arialFont, computerScore.ToString(), new Vector2(500, 10), Color.Yellow);

	spriteBatch.End();

	base.Draw(gameTime);
}

Στο μόνο που αξίζει να αναφερθούμε σχετικά με τη κλήση της μεθόδου DrawString() είναι το παράξενο όρισμα playerScore.ToString(), και το αντίστοιχο για το computerScore. Το playerScore είναι μια απλή μεταβλητή τύπου integer που φαίνεται να υποστηρίζει μια μέθοδο ToString(); Αυτό είναι μια ιδιότητα της C# σύμφωνα με την οποία ΟΛΟΙ οι τύποι δεδομένων κληρονομούν από την class Object, ακόμα και απλές μεταβλητές τύπου integer, float, char κλπ. (χωρίς να είναι απαραίτητα αντικείμενα!) Η κλάση αυτή υποστηρίζει τη μέθοδο ToString() η οποία μετατρέπει σε κείμενο (string) το περιεχόμενο του αντίστοιχου αντικειμένου. Στην δική μας περίπτωση θα μετατρέψει σε κείμενο το integer αριθμό που είναι αποθηκευμένος στις μεταβλητές του σκορ.

Κατά τα άλλα ορίζω ένα αντικείμενο τύπου Vector για να ορίσω τη θέση του κειμένου και θέτω το χρώμα του σε κίτρινο. Μπορούμε επιτέλους να παίξουμε το παιχνίδι με αλληλεπίδραση, συγκρούσεις και σκορ!

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

protected override void Update(GameTime gameTime)
{
				// υπόλοιπος κώδικας

	//ανίχνευση σύγκρουσης μπάλας με τα τοιχώματα του "γηπέδου"
	if (ball.Left < 0)
	{
		ball.X = 0;
		ballDirection.X = -ballDirection.X;
		computerScore++;
		ballSpeed = 5.0f;
	}
	else if (ball.Right > viewWidth)
	{
		ball.X = viewWidth - ball.Width ;
		ballDirection.X = -ballDirection.X;
		playerScore++;
		ballSpeed = 5.0f;
	}

			   // υπόλοιπος κώδικας

	//ανίχνευση σύγκρουσης μπάλας με τη ρακέτα παίκτη
	if (leftPaddle.Top <= ball.Top && ball.Bottom <= leftPaddle.Bottom)
	{
		if (ball.Left < leftPaddle.Right && ball.Right > leftPaddle.Right && ballDirection.X < 0)
		{
			ballDirection.X = -ballDirection.X;
			ballSpeed *= 1.1f;
		}
	}

	//ανίχνευση σύγκρουσης μπάλας με τη ρακέτα υπολογιστή
	if (rightPaddle.Top <= ball.Top && ball.Bottom <= rightPaddle.Bottom)
	{
		if (ball.Right > rightPaddle.Left && ball.Left < rightPaddle.Left && ballDirection.X > 0)
		{
			ballDirection.X = -ballDirection.X;
			ballSpeed *= 1.1f;
		}
	}

			   // υπόλοιπος κώδικας

	base.Update(gameTime);
}

Με κάθε σύγκρουση της μπάλας με την ρακέτα αυξάνουμε τη ταχύτητα της κατά 10%, και την επαναφέρουμε όταν διαπιστωθεί σύγκρουση της με αριστερό ή δεξί όριο του γηπέδου. Με τον τρόπο αυτό δίνουμε στο παίκτη μια ευκαιρία να σημειώσει κανένα πόντο (αν και ακόμα και έτσι με τσάκισε ο υπολογιστής).


Θα σταματήσουμε το tutorial εδώ, το παιχνίδι βρίσκεται σε αρκετά καλή κατάσταση, λείπουν οι ήχοι του οποίους θα προσθέσουμε στο επόμενο tutorial. Επιπλέον η ανίχνευση σύγκρουσης της μπάλας με τις ρακέτες και η αναπήδηση της από αυτές θα μπορούσε να γίνει καλύτερη. Θα το επιχειρήσουμε και αυτό στο επόμενο tutorial.

Μπορείτε να βρείτε το κώδικα του παιχνιδιού με τις σημερινές προσθήκες εδώ.

Ερωτήσεις, σχόλια, προτάσεις για βελτίωση στο γνωστό μέρος!


20 σχόλια προς “Hello Pong: μέρος 4ο”

  1. Σπύρος (spahar) είπε

    Δεν είναι λίγο άδικο που η ρακέτα αντιπάλου κινείται με ταχύτητα 3 φορές μικρότερη από τη δική μας; Φυσικά αν είχε μεγαλύτερη θα υπήρχε πρόβλημα αφού θα πήγαινε όλη την ώρα πάνω κάτω.

    Να προτείνω μια προσθήκη, ίσως να ήταν καλή ιδέα να βάλουμε μια μεταβλητή reaction_time για την ρακέτα του αντιπάλου, που θα δηλώνει μετά από πόσα frames (αφού η μπάλα χτύπησε τη ρακέτα μας) θα αρχίσει να κινείται η ρακέτα του αντιπάλου. Ίσως έτσι μπορέσουμε να υλοποιήσουμε και κάποιο είδος “επιπέδου δυσκολίας”, με reaction_time = 0 να είναι το hard και όσο το αυξάνουμε να γίνεται πιο εύκολο.

    Περιμένω το επόμενο tutorial! Αλήθεια έχετε σκεφτεί τι θα παρουσιάσετε μετά το Pong;

  2. darklynx είπε

    “Αυτό που μας ενδιαφέρει είναι η μεταβλητή FontName την οποία θα αλλάξουμε σε Arial από Kootenay (που το βρήκε αυτό το font;), και τη Size που θα την αυξήσουμε σε 60.”

    Τα περισσότερα από τα fonts που είναι προεγκατεστημένα στα Windows (και τα bitmap τους) είναι copyrighted και άρα δεν επιτρέπεται η αναδιανομή τους.Το XNA έχει αγοράσει από την Ascender Corporation τα δικαιώματα για μια σειρά από OpenType fonts,ένα εκ των οποίων και το παραπάνω τα οποία μπορούμε να τα χρησιμοποιήσουμε ελεύθερα.Περισσότερες πληροφορίες .

  3. darklynx είπε

    “Περιμένω το επόμενο tutorial! Αλήθεια έχετε σκεφτεί τι θα παρουσιάσετε μετά το Pong;”

    Pacman για semi-advanced AI (πολύ προχωρημένο παιχνίδι για την εποχή του) ή κάποιο side-scrolling shooter για να δούμε collision detection,parallax mapping και άλλα καλούδια :) .

  4. @spahar: καλή ιδέα η μεταβλητή reaction_time, αρκεί να είναι τυχαία η τιμή της, αλλιώς δεν θα κάνει μεγάλη διαφορά. Το βασικό πρόβλημα του παιχνιδιού όπως είναι τώρα είναι οτι η κίνηση της μπάλας είναι προβλέψιμη (αναπηδά με τον ίδιο τρόπο πάντα). Θα το βελτιώσουμε κάπως αυτό στο επόμενο tutorial.

    @darklynx: είναι μεγάλο το βήμα από το Pong στο Pacman! Θα συνεχίσουμε για λίγο καιρό με ολοένα και πιο πολύπλοκες παραλλαγές του Pong (πχ Arcanoid) μέχρι να αποκτήσουμε την άνεση να κάνουμε κάτι πιο σύνθετο.

  5. darklynx είπε

    Tetris τότε; ;)

  6. Σπύρος (spahar) είπε

    Να πω την αλήθεια και εγώ Pac Man σκέφτηκα αρχικά, αλλά το άλμα στην ΑΙ είνα αρκετά μεγάλο από το Pong. Εκεί λογικά θα χρειαστείς αλγόριθμο εύρεσης καλύτερου μονοπατιού για τα φαντάσματα, κάτι που είναι αρκετά πιο advanced από τη συμπεριφορά της ρακέτας. Το Tetris δεν είναι κακή ιδέα.

    Σχετικά με το reaction_time, το σκεφτόμουνα σαν επίπεδο δυσκολίας και όχι random, δηλαδή στο hard ο αντίπαλος αντιδρά άμεσα (άρα reaction_time = 0), στο normal ο αντίπαλος αντιδρά πιο αργά (raction_time = 10), στο easy κοιτάει τα περιστέρια :) (reactio_time = 25) κτλ.

    Να ρωτήσω κάτι άλλο για βελτίωση της ΑΙ. Διάβασα πριν κάτι μέρες ότι μια πιο φυσική συμπεριφορά της ρακέτας είναι μετά την απόκρουση να επιστρέφει στη μέση (ώστε να μπορεί να μεταβεί πιο γρήγορα σε οποιοδήποτε σημείο), κατά την απόκρουση από εμάς να υπολογιζεί που θα πάει η μπάλα και η ρακέτα του αντιπάλου να προσπαθεί να μεταβεί σε εκείνο το σημείο (και όχι να ακολουθεί την κίνηση της μπάλας). Θα δουλέψω λίγο αυτή την υλοποίηση, εσάς πως σας ακούγετε;

  7. darklynx είπε

    Να πω την αλήθεια και εγώ Pac Man σκέφτηκα αρχικά, αλλά το άλμα στην ΑΙ είνα αρκετά μεγάλο από το Pong. Εκεί λογικά θα χρειαστείς αλγόριθμο εύρεσης καλύτερου μονοπατιού για τα φαντάσματα

    Παραδόξως το μόνο στοιχείο της AI του Pacman που δεν αποτελεί πρόκληση είναι το pathfinding,μια που δεν προαποφασίζουν τα φαντάσματα την διαδρομή που θα ακολουθήσουν εξαρχής.Προχωράνε μπροστά και αν βρουν μια διασταύρωση κοιτούν ποια στροφή έχει την μικρότερη ευκλείδια απόσταση από τον στόχο και απλά στρίβουν προς τα εκεί.Όλα τα υπόλοιπα στη ΑΙ του pacman είναι η πρόκληση.
    Στο http://home.comcast.net/~jpittman2/pacman/pacmandossier.html έχει μια πολύ ωραία περιγραφή του παιχνιδιού σε όλο του το βάθος.

    Η ιδέα με τη ρακέτα είναι ενδιαφέρουσα αρκεί μην καταλήξει ο αντίπαλος πολύ έξυπνος για να είναι διασκεδαστικός.

  8. We want more!!!!

  9. “Διάβασα πριν κάτι μέρες ότι μια πιο φυσική συμπεριφορά της ρακέτας είναι μετά την απόκρουση να επιστρέφει στη μέση (ώστε να μπορεί να μεταβεί πιο γρήγορα σε οποιοδήποτε σημείο), κατά την απόκρουση από εμάς να υπολογιζεί που θα πάει η μπάλα και η ρακέτα του αντιπάλου να προσπαθεί να μεταβεί σε εκείνο το σημείο (και όχι να ακολουθεί την κίνηση της μπάλας). ”

    Ναι είναι καλή ιδέα! Στείλε μας τα συμπεράσματα σου από την υλοποίηση αυτής της ιδέας στο παιχνίδι.

  10. konsnos είπε

    Όταν η μπάλα πηγαίνει αργά στον οριζόντιο άξονα παρατηρείται ένα “ταρακούνημα” της ρακέτας του η/υ, αφού πηγαίνει πάνω-κάτω, λόγο της διαφοράς των 5px.
    Το διόρθωσα ως εξής:


    // ...
    protected override void Initialize()
    {
    // ...
    rightPaddleSpeed = 1.0f;
    // ...
    }
    //...
    protected override void Update(GameTime gameTime)
    {
    // ...
    // Κίνηση ρακέτας η/υ
    for (int i = 0; i < 5; i++)
    {
    int sign = Math.Sign((ball.Top + ball.Height / 2) - (rightPaddle.Top + rightPaddle.Height / 2));
    rightPaddle.Y += (int)(sign * rightPaddleSpeed);
    }
    // ...
    }
    // ...

    Πως σας φαίνεται;

  11. konsnos είπε

    ΥΓ> Τον κώδικα τον έβαλα μέσα στα tags code, και για αυτό φαίνεται έτσι. Τι να χρησιμοποιώ για να φαίνεται καλύτερα; Ας τροποποιηθεί για να γίνει πιο κατανοητό παρακαλώ.

  12. konsnos είπε

    ΥΓ2> Μόλις το σκέφτηκα. Μια πολύ μικρή τροποποίηση για να γίνει λίγο πιο κατανοητό. Η μεταβλητή rightPaddleSpeed ανέβηκε ως παράμετρος του loop.

    for (int i = 0; i < rightPaddleSpeed; i++)
    {
    int sign = Math.Sign((ball.Top + ball.Height / 2) - (rightPaddle.Top + rightPaddle.Height / 2));
    // Υπολογίζει αν το κέντρο της μπάλας είναι πάνω ή κάτω από το κέντρο της ρακέτας
    rightPaddle.Y += (int)(sign * 1);
    }

  13. Κώστας Αναγνώστου είπε

    @konsnos: σωστή η παρατήρηση σου. Βασικά το πρόβλημα με το τρεμόπαιγμα θα εμφανίζεται οποτεδήποτε η ταχύτητα της ρακέτας (rightPaddleSpeed) είναι μεγαλύτερη από την ταχύτητα της μπάλας. Αυτό συμβαίνει γιατί τη προσπερνά και μετά προσπαθεί να ευθυγραμμίσει πάλι τα 2 κέντρα με αποτέλεσμα να ταλαντεύεται πάνω-κάτω.

    Η λύση που δίνεις είναι σωστή σαν ιδέα, μιας και θα φέρει τα δυο κέντρα στο ίδιο ύψος και θα σταματήσει εκεί (το sign θα γίνει 0). Όμως η υλοποίηση μπορεί να γίνει και καλύτερα. Μιας και το loop που προτείνεις θα τρέχει σε ένα καρέ, θα μπορούσαμε να υπολογίσουμε την απόσταση των 2 κέντρων και να φέρουμε τη ρακέτα στο ίδιο ύψος με την μπάλα χωρίς τη χρήση loop.

    Γενικά όμως, με τον απλοικό τρόπο που κυνηγά η ρακέτα την μπάλα δεν έχει νόημα η ταχύτητα της να είναι μεγαλύτερη από αυτή της μπάλας. Μπορούμε κάλλιστα να τις εξισώσουμε.

  14. Σπύρος (spahar) είπε

    “Ναι είναι καλή ιδέα! Στείλε μας τα συμπεράσματα σου από την υλοποίηση αυτής της ιδέας στο παιχνίδι.”

    To υλοποίησα ως εξής: Δημιούργησα μια συνάρτηση η οποία καλείται όταν η μπάλα χτυπάει στη ρακέτα του παίχτη ή στο τοίχο από τη μεριά του παίχτη. Αυτή η συνάρτηση υπολογίζει το σημείο στο οποίο θα καταλήξει η μπάλα από τη μεριά του αντιπάλου.

    Η ρακέτα του αντιπάλου πάντα προσπαθεί να πάει σε ένα συγκεκριμένο στόχο. Όταν η μπάλα χτυπάει στη ρακέτα του αντιπάλου ή στο τοίχο (από τη μεριά του) τότε ο στόχος αυτός είναι το μέσο (viewHeight/2), με λίγα λόγια η ρακέτα επιστρέφει στη μέση. Όταν η μπάλα χτυπάει στη ρακέτα του παίχτη ή στο τοίχο του ο στόχος υπολογίζεται από την παραπάνω συνάρτηση.

    Με αυτά μόνο ο υπολογιστής είναι ανίκητος. Ακόμα και με την προσθήκη της ταχύτητας που αυξάνεται ο υπολογιστής πάντα προλαβαίνει να πάει στη σωστή θέση.

    Για αυτό πρόσθεσα και το reaction_time, το οποίο το έβαλα να παίρνει random τιμές. To πόσο δύσκολο θα είναι το παιχνίδι κρίνεται από το διάστημα που παίρνει τιμές η reaction_time. Σε γενικές γραμμές η συμπεριφορά της ρακέτας είναι πιο φυσική και πιο στρατηγική. Το ζήτημα είναι να βρω τον ιδανικό τρόπο για τον υπολογισμό του reaction_time. Προς το παρόν υπολογίζω πόσο “χρόνο” χρειάζεται η μπάλα για να πάει από τη μία άκρη στην άλλη, και θέτω τo reaction_time ανάλογα, πχ στο διάστημα (time/2, 4*time/5) θα χαρακτήριζα το παιχνίδι normal σε δυσκολία.

    Αν σας ενδιαφέρει θα ανεβάσω και τον κώδικα.

  15. Κώστας Αναγνώστου είπε

    Μάλιστα, ο υπολογιστής προλαβαίνει τη μπάλα ακόμα και αν η ταχύτητα της ρακέτας του είναι μικρή (πχ 2);

    Βασικά είναι καλό όσοι ενδιαφέρονται και πειραματίζονται πάνω στο κώδικα να ανεβάζουν τη δική τους έκδοση του παιχνιδιού με βελτιώσεις και παραλλαγές κάπου έτσι ώστε και οι υπόλοιποι αναγνώστες του blog να μπορούν να δουν τις ιδέες τους.

    Έχει κανείς υπόψη καμία ιστοσελίδα που να το υποστηρίζει αυτό (κάτι σαν code repository?).

  16. Σπύρος (spahar) είπε

    “Μάλιστα, ο υπολογιστής προλαβαίνει τη μπάλα ακόμα και αν η ταχύτητα της ρακέτας του είναι μικρή (πχ 2);”

    Λογικά σε αυτή τη περίπτωση δε θα τη προλαβαίνει πάντα. Εγώ την έχω την ταχύτητα στο 10 (του παίχτη στο 15), και αν λάβουμε υπόψη ότι η ρακέτα του υπολογιστή με αυτή την υλοποίηση καλείται να καλύψει απόσταση <= 260 (η ρακέτα βρίσκεται πάντα στο κέντρο), ενώ η μπάλα καλύπτει οριζόντια απόσταση περίπου 700, θα πρέπει η ταχύτητα της μπάλας να γίνει 3πλάσια+ από τη ταχύτητα της ρακέτας (και αυτό όταν η ρακέτα θα πρέπει να πάει μέχρι την άκρη).

    Μόλις καθαρίσω λίγο τον κώδικα και καταλήξω σε ένα διάστημα για την random θα ανεβάσω τον κώδικα.

  17. konsnos είπε

    “Έχει κανείς υπόψη καμία ιστοσελίδα που να το υποστηρίζει αυτό (κάτι σαν code repository?).”

    http://pastebin.com/

  18. darklynx είπε

    “Έχει κανείς υπόψη καμία ιστοσελίδα που να το υποστηρίζει αυτό (κάτι σαν code repository?).”

    Πήρα το θάρρος να φτιάξω μια καταχώρηση στο Google Code ( http://code.google.com/p/vglcr/ ) η οποία θα λειτουργήσει ως sandbox/code repository για όσους θέλουν να βρίσκουν συγκεντρωμένο τον κώδικα από τα μαθήματα του blog,να ανεβάζουν τις δικές τους βελτιώσεις στον κώδικα ή στο artwork του παιχνιδιού.Όλα αυτά εφόσον σας βρίσκουν σύμφωνο και σας παραχωρηθεί η κυριότητα του project φυσικά.

  19. Σπύρος (spahar) είπε

    Να ρωτήσω κάτι σχετικά με το XNA, όταν πατάς F5 για να τρέξεις τον κώδικα, εκτός από το παράθυρο με το παιχνίδι, αλλάζει και το interface από πίσω. Για την ακρίβεια εμφανίζονται δύο παράθυρα στο κάτω μέρος, το Locals και το Call Stack. Υπάρχει τρόπος σε αυτά ή σε κάποιο άλλο παράθυρο να εμφανίζονται οι μεταβλητές με τις τιμές τους;

  20. Κώστας Αναγνώστου είπε

    @spahar: Για εκσφαλμάτωση (στα ελληνικά debugging) του κώδικα θα χρειαστείς να τοποθετήσεις breakpoints στα σχετικά σημεία του κώδικα. Στο ύψος του κώδικα που θες να ελέγξεις κάνε ένα αριστερό κλικ με το ποντίκι στην μπεζ λωρίδα που υπάρχει στα αριστερά του (λίγο πριν το πλαίσιο του παραθύρου), και ενας κόκκινος κύκλος θα εμφανιστεί. Την επόμενη φορά που το πρόγραμμα θα περάσει από αυτό το σημείο η εκτέλεση του κώδικα θα σταματήσει. Αυτό θα το καταλάβεις από το κίτρινο βελάκι που θα εμφανιστεί στη θέση του breakpoint. Μόνο τότε μπορείς να δείς τις τιμές των μεταβλητών.

    Το παράθυρο Local που αναφέρεις δείχνει τις τιμές των μεταβλητών που είναι ορατές σε αυτό το σημείο του κώδικα που τοποθέτησες το breakpoint (global μεταβλητές και ότι δήλωσες μέσα στην μέθοδο). Αν θες να δεις τη τιμή μιας συγκεκριμένης μεταβλητής, κάνε δεξί κλικ επάνω της και από μενού που θα εμφανιστεί επέλεξε Add Watch. Στο κάτω μέρος της οθόνης πρέπει να έχει εμφανιστεί τώρα ένα παράθυρο με το όνομα Watch στο οποίο θα μπορείς να δείς τις τιμές όλων των μεταβλητών που έχεις περάσει από την διαδικασία αυτή (Add Watch δηλαδή). Αν το παράθυρο αυτό δεν είναι ορατό, από το κυρίως μενού του Visual Studio επέλεξε Debug/Windows/Watch.

    Επιπλέον με την χρήση του F10 (και όντας σταματημένη η εκτέλεση του κώδικα) μπορείς να προχωρήσεις στην εκτέλεση της επόμενης εντολής στο κώδικά (step execution). Για να βγεις από το debugging mode βγάλε όλα τα breakpoints (κάνε αριστερό κλίκ επάνω τους) και πάτα το F5.

    Πάντως αξίζει να συμπεριλάβω ένα άρθρο για debugging στο μέλλον, μπήκε και αυτό στη λίστα! :-)

    PS: Η διαδικασία που περιέγραψα αφορά το Visual Studio και όχι το XNA. Και μια οποιαδήποτε άλλη εφαρμογή σε C# (και C++ και Visual Basic) να αναπτύξουμε, την ίδια διαδικασία θα ακολουθήσουμε.

Υποβολή απάντησης

XHTML: Μπορείτε να χρησιμοποιήσετε αυτές τις ετικέτες: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <pre> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>