Arkanoid: Game State Management μέρος 2ο
Δημοσιεύθηκε από Κώστας Αναγνώστου στο Οκτωβρίου 6, 2009
Στο προηγούμενο tutorial σχεδιάσαμε το game state management του Arkanoid και το υλοποιήσαμε για την διαδικασία Update του παιχνιδιού. Σήμερα θα το υλοποιήσουμε και για την διαδικασία Draw, συμπληρώνοντας παράλληλα το παιχνίδι με Εισαγωγική οθόνη, λειτουργία Pause και οθόνη επιτυχίας ή αποτυχίας του παίκτη.
Είχαμε αναφέρει ότι το game state management υλοποιείται στην απλή μορφή του με ένα switch-statement. Το ίδιο ακριβώς swith-statement (το σκελετό δηλαδή) που χρησιμοποιήσαμε στην Update θα χρησιμοποιήσουμε και στην Draw. Πριν αρχίσουμε όμως, χάριν ευαναγνωσιμότητας(!) του κώδικα, θα κάνουμε και πάλι μια μικρή αναδιοργάνωση του κώδικα της Draw, δημιουργώντας μια νέα μέθοδο renderWorld() η οποία θα απεικονίζει όλα τα αντικείμενα του παιχνιδιού (τουβλάκια, ρακέτα, μπάλα και σκορ) στην οθόνη:
private void renderWorld()
{
string message = String.Format("Lives: {0}, Score: {1}", lives, score);
spriteBatch.DrawString(font, message, new Vector2(10, 10), Color.Yellow);
foreach (Brick brick in bricks)
{
brick.Draw(spriteBatch);
}
spriteBatch.Draw(whiteTile, paddle, Color.White);
spriteBatch.Draw(whiteTile, ball, Color.Yellow);
}
Όπως και στην updateWorld(), καμιά νέα λειτουργία δεν συμπεριλαμβάνεται στην renderWorld(). Ο κώδικας της Draw() γίνεται τώρα:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
renderWorld();
spriteBatch.End();
base.Draw(gameTime);
}
Τίποτα το συγκλονιστικό μέχρις εδώ. Προσθέτουμε τώρα το switch-statement που αναφέραμε παραπάνω:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
switch (gameState)
{
case GameState.Intro:
break;
case GameState.Playing:
renderWorld();
break;
case GameState.Paused:
break;
case GameState.Won:
break;
case GameState.Lost:
break;
}
spriteBatch.End();
base.Draw(gameTime);
}
Αν παρατηρήσατε, μετακίνησα τη renderWorld() στο block του GameState.Playing, μια και λογικά πρέπει να απεικονίζουμε τα αντικείμενα του παιχνιδιού όταν ο παίκτης παίζει. Ας προσθέσουμε την εισαγωγική οθόνη τώρα. Σε ένα οποιοδήποτε σχεδιαστικό πρόγραμμα φτιάχνουμε με μεγάλα γράμματα το λογότυπο του παιχνιδιού. Εγώ το έφτιαξα σε Fireworks και το έσωσα ως logo.png. Μπορείτε επιπλέον να το αποθηκεύσετε ως jpg, gif και bmp.

Προσθέτουμε το αρχείο του λογότυπου στο Content project του παιχνιδιού (δεξί κλικ επάνω στο όνομα, Add/New Item).
Για να χρησιμοποιήσουμε την υφή στο παιχνίδι θα ορίσουμε μια μεταβλητή logoTexture στην κυρίως κλάση του παιχνιδιού:
public class Game1 : Microsoft.Xna.Framework.Game
{
…
Texture2D logoTexture;
…
Στην LoadContent φορτώνουμε την υφή ως συνήθως:
protected override void LoadContent()
{
…
whiteTile = Content.Load<Texture2D>("white_tile");
logoTexture = Content.Load<Texture2D>("logo");
…
}
Στην Draw τώρα, στο block GameState.Intro, απλά απεικονίζουμε την υφή με τη χρήση της spritebatch στην τοποθεσία (50,150):
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
switch (gameState)
{
case GameState.Intro:
spriteBatch.Draw(logoTexture, new Vector2(50,150), Color.White);
break;
case GameState.Playing:
renderWorld();
break;
case GameState.Paused:
break;
case GameState.Won:
break;
case GameState.Lost:
break;
}
spriteBatch.End();
base.Draw(gameTime);
}
Προσοχή πρέπει να δοθεί στο τρόπο χρήσης της spriteBatch.Draw (), δεν έχω ορίσει Rectangle μέσα στο οποίο θα απεικονιστεί η υφή, την τοποθεσία στην οποία θα απεικονιστεί. Για το λόγο αυτό η spriteBatch.Draw() θα την απεικονίσει στο φυσικό της μέγεθος (χωρίς μεγέθυνση ή σμίκρυνση).
Αν τρέξουμε τώρα το παιχνίδι με F5, θα δούμε την εισαγωγική οθόνη του παιχνιδιού με το λογότυπο:

Επειδή έχουμε υλοποιήσει ήδη το state machine στην Update, αν πατήσουμε ENTER το παιχνίδι θα αρχίσει. Όμως ο παίκτης δεν το ξέρει αυτό και θα πρέπει να τον ενημερώσουμε με σχετικό μήνυμα. Γνωρίζουμε ήδη πώς να απεικονίζουμε κείμενο στην οθόνη με τη χρήση της spriteBatch.DrawString. Επειδή στο παιχνίδι θέλω σε πολλές περιπτώσεις να απεικονίζω κείμενο στο κέντρο της οθόνης, όσον αφορά τη Χ διάσταση σε οποιοδήποτε ύψος Υ, θα φτιάξω μια βοηθητική μέθοδο που θα το κάνει αυτόματα αυτό:
private void renderStringCentered(string message, int Y, Color color)
{
Vector2 msgSize = font.MeasureString(message);
spriteBatch.DrawString(font, message, new Vector2((viewWidth - msgSize.X) / 2, Y), color);
}
Η μέθοδος renderStringCentered() παίρνει ως όρισμα το κείμενο, το ύψος Y στο οποίο θέλω να εμφανίζεται και το χρώμα του. Στην συνέχεια υπολογίζει το πλάτος του κειμένου με τη χρήση της μεθόδου font.MeasureString(). Το font είναι το αντικείμενο που περιέχει τη γραμματοσειρά που φορτώσαμε από το δίσκο και που χρησιμοποιούμε για να απεικονίσουμε κείμενο. Το αντικείμενο αυτό ξέρει το πλάτος και το ύψος κάθε χαρακτήρα της γραμματοσειράς. Οπότε αν του δώσουμε ως όρισμα το κείμενο που θέλουμε να απεικονίσουμε αυτό θα μας επιστρέψει το ύψος και το πλάτος του σε pixel, σε μια μεταβλητή τύπου Vector2. Τέλος η renderStringCentered() απεικονίζει το κείμενο στην οθόνη με τη χρήση της spriteBatch.DrawString() χρησιμοποιώντας το πλάτος του κειμένου που υπολογίσαμε προηγουμένως (msgSize.X) για να κεντράρει το κείμενο στην οθόνη.
Επιπλέον, για να κάνω την εισαγωγική οθόνη σχετική με το παιχνίδι θα απεικονίσω πίσω από το λογότυπο μερικές σειρές τουβλάκια:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
switch (gameState)
{
case GameState.Intro:
for (int j = 0; j < 10; j++)
{
for (int i = 0; i < bricksPerRow; i++)
{
Rectangle rect = new Rectangle(i * (brickWidth + brickSpacing),
100 + j * (brickHeight + brickSpacing),
brickWidth, brickHeight);
spriteBatch.Draw(whiteTile, rect, Color.LimeGreen);
}
}
spriteBatch.Draw(logoTexture, new Vector2(50,150), Color.White);
renderStringCentered( "Press [Enter] to begin",430, Color.White);
break;
case GameState.Playing:
renderWorld();
break;
case GameState.Paused:
break;
case GameState.Won:
break;
case GameState.Lost:
break;
}
spriteBatch.End();
base.Draw(gameTime);
}
Απεικονίζουμε 10 σειρές τουβλάκια πίσω από το λογότυπο με πράσινο χρώμα. Επιπλέον απεικονίζουμε τη προτροπή στο χρήστη να πατήσει το ENTER για να ξεκινήσει το παιχνίδι (προτάσεις για καλύτερες επιλογές χρώματων δεκτές! J ).

Έχουμε μέχρι στιγμής υλοποιήσει τις καταστάσεις GameState.Intro και GameState.Playing. Οι υπόλοιπες καταστάσεις είναι ακόμα πιο απλές. Για τη Paused το μόνο που έχουμε να κάνουμε είναι να δείξουμε στο χρήστη ότι το παιχνίδι έχει «παγώσει».
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
switch (gameState)
{
//υπόλοιπος κώδικας
case GameState.Paused:
renderWorld();
renderStringCentered("PAUSED!", 270, Color.Yellow);
break;
//υπόλοιπος κώδικας
}
spriteBatch.End();
base.Draw(gameTime);
}
Η κατάσταση αυτή μπορεί να λάβει χώρα όταν ο παίκτης παίζει το παιχνίδι, οπότε είναι καλό μαζί με το μήνυμα να απεικονίζουμε στο φόντο και τα αντικείμενα του παιχνιδιού (αλλιώς θα φαινόταν παράξενο να βλέπει το «PAUSED» σε μια μαύρη οθόνη). Το μόνο που έχουμε να κάνουμε είναι να καλέσουμε τη renderWorld() λίγο πριν απεικονίσουμε το κείμενο. Επειδή στην Update δεν ανανεώνουμε τις θέσεις των αντικειμένων του παιχνιδιού όταν το παιχνίδι βρίσκεται σε κατάσταση GameState.Paused, τα αντικείμενα δεν μετακινούνται και έτσι απλά υλοποιείται η λειτουργία Paused. Το παιχνίδι ξαναρχίζει όταν ο παίκτης πατήσει το πλήκτρο P ξανά.
Οι καταστάσεις GameState.Won και GameState.Lost έχουν ακριβώς την ίδια λογική:
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
switch (gameState)
{
//υπόλοιπος κώδικας
case GameState.Won:
renderWorld();
renderStringCentered("YOU WIN!", 270, Color.Yellow);
renderStringCentered("Press Enter to play again", 300, Color.White);
break;
case GameState.Lost:
renderWorld();
renderStringCentered("YOU LOSE!", 270, Color.Yellow);
renderStringCentered("Press Enter to play again", 300, Color.White);
break;
}
spriteBatch.End();
base.Draw(gameTime);
}
Στις περιπτώσεις αυτές εμφανίζουμε το ανάλογο μήνυμα επιτυχίας ή αποτυχίας στο παίκτη και τον προτρέπουμε να πατήσει ENTER για να παίξει ξανά. Αν πατήσει το ESC το παιχνίδι θα τον μεταφέρει στην Εισαγωγική οθόνη.
Αυτό ήταν, το game state management, μαζί με τις διάφορες οθόνες του παιχνιδιού (σχεδόν) υλοποιήθηκε. Μένουν 2-3 (ουσιαστικές) λεπτομέρειες. Καταρχάς δεν έχουμε ορίσει ακόμα πως το παιχνίδι θα μεταβαίνει στη κατάσταση Won ή Lost. Η Lost είναι απλή, το μόνο που έχουμε να κάνουμε είναι να ελέγξουμε στην updateWorld αν ο παίκτης έχει χάσει όλες τις ζωές του.
private void updateWorld()
{
//υπόλοιπος κώδικας
//check if player lost all lives
if (lives <= 0)
{
gameState = GameState.Lost;
}
}
Το αν κέρδισε είναι λίγο πιο πολύπλοκο, πρέπει να διαπιστώσουμε αν όλα τα τουβλάκια στην οθόνη έχουν καταστραφεί. Ο πιο απλός τρόπος να υλοποιήσουμε αυτή τη λειτουργία είναι να ορίσουμε μια μεταβλητή numOfVisibleBricks στη κλαση Game1 που να μειώνεται κατά ένα κάθε φορά που καταστρέφεται ένα τουβλάκι μέχρι να γίνει 0.
public class Game1 : Microsoft.Xna.Framework.Game
{
int numOfVisibleBricks;
Στην LoadContent αυξάνω τη τιμή της μεταβλητής κάθε φορά που δημιουργώ ένα νέο τουβλάκι με τη χρήση του τελεστή new:
protected override void LoadContent()
{
//υπόλοιπος κώδικας
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++;
}
}
}
Τέλος στην updateWorld, μειώνω την τιμή της κατά ένα, κάθε φορά που διαπιστώνω σύγκρουση μπάλας με τουβλάκι.
private void updateWorld()
{
//υπόλοιπος κώδικας
//check ball-brick collision
foreach (Brick brick in bricks)
{
if (brick.CheckHit(ball))
{
numOfVisibleBricks--;
ballDirection.Y = -ballDirection.Y;
score += 1000;
if (score % 10000 == 0)
{
ballSpeed += 1;
}
break;
}
}
//check if player lost all lives
if (lives <= 0)
{
gameState = GameState.Lost;
}
else if (numOfVisibleBricks <= 0)
{
gameState = GameState.Won;
}
}
Παράλληλα, στο τέλος της updateWorld, ελέγχω μήπως ο αριθμός των ορατών bricks έχει φτάσει στο μηδέν. Σε μια τέτοια περίπτωση αλλάζω το state του παιχνιδιού σε GameState.Won και το παιχνίδι τελειώνει.
Υπάρχουν και πιο κομψοί τρόποι να υλοποιηθεί αυτό (από μια global μεταβλητή εννοώ). Γενικά στα παιχνίδια στα οποία υπάρχουν πολλά «όμοια» αντικείμενα (διαστημόπλοια, εχθροί, τουβλάκια, συστήματα σωματιδίων κλπ), συνήθως ορίζουμε ένα αντικείμενο-manager το οποίο αναλαμβάνει τη διαχείριση τους, το οποίο είναι υπεύθυνο και για πληροφορίες όπως αυτή που ζητάμε. Αυτό είναι όμως κάτι που θα γνωρίσουμε σε επόμενο παιχνίδι.
Αν τρέξετε το παιχνίδι τώρα και πατήσετε ΕΝΤΕR για να αρχίσει και P για να παγώσει, θα παρατηρήσετε ότι άλλες φορές ενεργοποιείται το πάγωμα και άλλες όχι. Αυτό συμβαίνει και με άλλες λειτουργίες στο παιχνίδι και οφείλεται στο ότι το παιχνίδι αλλάζει πολλές φορές state όσο ένα πλήκτρο (το P στην περίπτωση μας είναι πατημένο). Πρέπει συνεπώς να υλοποιήσουμε μια λειτουργία που να μου επιστρέφει true όταν το κάποιο πλήκτρο πατήθηκε για πρώτη φορά και false όσο είναι πατημένο.
Καταρχάς πρέπει να ορίσουμε μια μεταβλητή previousKeyboardState η οποία θα κρατά το keyboardState του προηγούμενου καρέ (frame) του παιχνιδιού:
public class Game1 : Microsoft.Xna.Framework.Game
{
int numOfVisibleBricks;
GameState gameState;
KeyboardState keyboardState;
KeyboardState previousKeyboardState;
Στην συνέχεια, πριν ολοκληρώσουμε την Update του τρέχοντος καρέ, αποθηκεύουμε το keyboardState στην μεταβλητή αυτή:
protected override void Update(GameTime gameTime)
{
//υπόλοιπος κώδικας
previousKeyboardState = keyboardState;
base.Update(gameTime);
}
Τώρα είμαστε έτοιμοι να υλοποιήσουμε τη μέθοδο που να επιστρέφει true μόνο όταν το δεδομένο πληκτρο key πατιέται για πρώτη φορά.
private bool keyPressed(Keys key)
{
if (previousKeyboardState.IsKeyUp(key) && keyboardState.IsKeyDown(key))
{
return true;
}
return false;
}
Αν στο προηγούμενο καρέ το πλήκτρο key ήταν ελεύθερο (previousKeyboardState.IsKeyUp(key)) και στο τρέχον καρέ είναι πατημένο (keyboardState.IsKeyDown(key)), τότε επιστρέφουμε true, διαφορετικά false.
Θα αξιοποιήσουμε τη μέθοδο αυτή στην Update σε 3 game states:
protected override void Update(GameTime gameTime)
{
keyboardState = Keyboard.GetState();
switch (gameState)
{
case GameState.Intro:
if (keyPressed(Keys.Escape))
{
this.Exit();
}
else if (keyboardState.IsKeyDown(Keys.Enter))
{
gameState = GameState.Playing;
}
break;
case GameState.Playing:
if (keyboardState.IsKeyDown(Keys.Escape))
{
gameState = GameState.Intro;
}
else if (keyPressed(Keys.P))
{
gameState = GameState.Paused;
}
else
{
updateWorld();
}
break;
case GameState.Paused:
if (keyPressed(Keys.P))
{
gameState = GameState.Playing;
}
break;
//υπόλοιπος κώδικας
}
previousKeyboardState = keyboardState;
base.Update(gameTime);
}
Στο GameState.Intro, επειδή φτάνουμε στην κατάσταση αυτή με ESC από άλλες καταστάσεις (την ώρα που παίζει ο χρήστης, ή βρίσκεται σε οθόνη αποτυχίας), πρέπει να εξασφαλίσουμε ότι ο χρήστης πάτησε μια ακόμα φορά το ESC πριν κλείσουμε εντελώς το παιχνίδι (αλλιώς θα βγει από το παιχνίδι χωρίς να το έχει επιλέξει). Τέλος για τη λειτουργία Pause μιλήσαμε, πρέπει να ενεργοποιείται και απενεργοποιείται με διακριτά πατήματα του πλήκτρου P.
Όποιος έχει μείνει ζωντανός μέχρι σ’αυτό το σημείο το έχει κάνει από την αστείρευτη επιθυμία και επιμονή του να δει το παιχνίδι ολοκληρωμένο οπότε δεν μπορώ να του το στερήσω αυτό. Οι υπόλοιποι έχουν έτσι κι αλλιώς αποκοιμηθεί ή τα παρατήσει οπότε δεν θα τους πειράξει J. Μένει ακόμα κάτι να υλοποιήσουμε πριν ολοκληρώσουμε το game state management και αυτό είναι η δυνατότητα του παίκτη να ξαναρχίζει τη πίστα από την αρχή. Χρειαζόμαστε ένα τρόπο να απεικονίζουμε ξανά τα τουβλάκια στις αρχικές τους θέσεις, να μηδενίζουμε το σκορ και να επαναρχικοποιουμε τις ζωές του παίκτη και να τοποθετούμε ρακέτα και μπάλα εκεί που ήταν αρχικά.
Μέχρι στιγμής αυτές τις λειτουργίες τις κάναμε εν μέρει στην μέθοδο Initialize() και εν μέρει στην LoadContent(). Αυτό που θα πρέπει να γίνει είναι να μεταφέρουμε όλες τις αρχικοποιήσεις σε μια μέθοδο την οποία θα μπορούμε να καλούμε από οπουδήποτε:
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;
numOfVisibleBricks = 0;
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++;
}
}
}
Η resetGame() πραγματοποιεί όλες τις αρχικοποιήσεις που αναφέραμε. Προσοχή πρέπει να δοθεί στο ότι οι αντίστοιχες αρχικοποιήσεις έχουν αφαιρεθεί από τις Initialize() και LoadContent().
Την resetGame θα την καλέσουμε από την Update κάθε φορά που θέλουμε να αρχικοποιήσουμε τη πίστα:
protected override void Update(GameTime gameTime)
{
keyboardState = Keyboard.GetState();
switch (gameState)
{
case GameState.Intro:
if (keyPressed(Keys.Escape))
{
this.Exit();
}
else if (keyboardState.IsKeyDown(Keys.Enter))
{
resetGame();
gameState = GameState.Playing;
}
break;
//υπόλοιπος κώδικας
case GameState.Won:
if (keyboardState.IsKeyDown(Keys.Escape))
{
gameState = GameState.Intro;
}
else if (keyboardState.IsKeyDown(Keys.Enter))
{
resetGame();
gameState = GameState.Playing;
}
break;
case GameState.Lost:
if (keyboardState.IsKeyDown(Keys.Escape))
{
gameState = GameState.Intro;
}
else if (keyboardState.IsKeyDown(Keys.Enter))
{
resetGame();
gameState = GameState.Playing;
}
break;
}
previousKeyboardState = keyboardState;
base.Update(gameTime);
}
Τη πίστα την αρχικοποιούμε στο block GameState.Intro, όταν ο παίκτης πατήσει το ENTER για να αρχίσει το παιχνίδι, όπως και στα block GameState.Won και GameState.Lost για τον ίδιο λόγο (να δοκιμάσει την πίστα ξανά).
Αυτό ήταν, τελειώσαμε για σήμερα (αλήθεια!). Το παιχνίδι είναι πλήρες από λειτουργίες και ο παίκτης μπορεί να δοκιμάσει τη πίστα όσες φορές θέλει.
Σήμερα καλύψαμε πολύ έδαφος. Το συμπέρασμα της ημέρας πρέπει να είναι ότι όσο απλό και αν είναι ένα παιχνίδι, απαιτεί πολύ συνοδευτικό κώδικα για να υλοποιήθούν λειτουργίες που δεν έχουν άμεση σχέση με τη διαδικασία παιχνιδιού, άλλα είναι απαραίτητες για την καλή εμπειρία του χρήστη.
Ο κώδικας του tutorial βρίσκεται στο Code Repository μέσω SVN ή και σε zip μορφή. Στο επόμενο tutorial θα δούμε πως μπορούμε να φτιάξουμε διαφορετικές πίστες στο παιχνίδι.

darklynx είπε
Και έτσι φτιάχνεται ένα πλήρες παιχνίδι…
Είναι εύκολο όταν κάποιος φτιάξει ένα παιχνίδι που “δουλεύει” να αμελήσει να ασχοληθεί με τις μικρές λεπτομέρειες (μενού,λειτουργία pause,πίνακας σκορ κ.τ.λ) λαμβάνοντάς τα ως βαρετές λεπτομέρειες.Όμως εκτός του ότι αυτές οι λεπτομέρειες θα ξεχωρίσουν το παιχνίδι του από τον σωρό των άλλων που κυκλοφορούν εκεί έξω (και εν τέλει μαρτυρούν την προχειρότητα ή όχι του δημιουργήματος) συχνά αποτελούν μεγαλύτερη πρόκληση από το παιχνίδι το ίδιο,φέρνοντας στην επιφάνεια δυσκολίες ή και σχεδιαστικά λάθη που δεν είχαν ληφθεί υπόψη.
Να ρωτήσω τι περιλαμβάνει το “μενού” των επερχόμενων tutorials;
Κώστας Αναγνώστου είπε
Θα ασχοληθούμε λίγο ακόμα με το Arkanoid για να δούμε πως γίνεται ο σχεδιασμός μιας πίστας. Μετά έλεγα να ασχοληθούμε με κάποιο περισσότερο action παιχνίδι όπως το Space Invaders, εκτός αν οι αναγνώστες του blog αισθάνονται έτοιμοι για την μετάβαση στην τρίτη διάσταση.
konsnos είπε
Πολύ καλός οδηγός! Μπράβο.
Μετά από λίγο beta testing φαίνεται πως όταν ξεπεράσουμε το σκορ των 50.000 πόντων, αυτή κολλάει με το πάνω τοίχωμα (ή και τα υπόλοιπα; ). Δεν κατάλαβα τον λόγο, αλλά θα κοιτάξω να το διορθώσω όταν βρω χρόνο.
Με τις μεθόδους (functions) που παρουσίασες νομίζω πως μπήκαμε στο νόημα του πως να δημιουργήσουμε ένα κομμάτι κώδικα και να το κάνουμε εύκολα διαθέσιμο και στους υπόλοιπους προγραμματιστές της ομάδας, με μικρές αλλαγές στον κορμό.
konsnos είπε
Στο προηγούμενο σχόλιό μου εννοούσα την μπάλα.
Επίσης για την αναφορά στο μενού των επόμενων οδηγών, ίσως θα μπορούσαμε κατά το intro να παίξουμε μια background μουσική; Ασχολήθηκα με το θέμα με την κλάση song, και θα ήταν πολύ ενδιαφέρον να την μελετήσουμε.
Κώστας Αναγνώστου είπε
“Μετά από λίγο beta testing φαίνεται πως όταν ξεπεράσουμε το σκορ των 50.000 πόντων, αυτή κολλάει με το πάνω τοίχωμα (ή και τα υπόλοιπα;”
Παράξενο αυτό… πρέπει να το διερευνήσουμε.
Για τη μουσική, ναι, φυσικά και μπορούμε να παίζουμε μουσική στο intro.
Γιώργος είπε
” Μετά έλεγα να ασχοληθούμε με κάποιο περισσότερο action παιχνίδι όπως το Space Invaders, εκτός αν οι αναγνώστες του blog αισθάνονται έτοιμοι για την μετάβαση στην τρίτη διάσταση.
”
Ίσως γίνεται το επόμενο παιχνίδι να είναι ας πούμε το space invaders αλλά με τρισδιάστατα γραφικά. Δηλαδή η κίνηση να είναι στις δύο διαστάσεις αλλά τα γραφικά τρισδιάστατα, για να είναι πιο ομαλή η μετάβαση. Έτσι νομίζω ήταν και το παιχνίδι στο tutorial με την openGL.
konsnos είπε
Προσωπικά είμαι κατά του 3D από τώρα. Θα προτιμούσα να συνεχίσουμε με 2Δ, όπως να φτιάξουμε παιχνίδι platform με sprites, και levels, και να κάνουμε μια προσπάθεια για συνεργατική ανάπτυξη κώδικα.
Σπύρος (spahar) είπε
Θα συμφωνήσω και εγώ με την ιδέα ενός 2D platformer, να δούμε sprites, ΑΙ κτλ.