Το προφίλ μας στο Google Plus
0

Όταν μαλώνουν τα textures

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

Αρχικά θέλουμε να σας ζητήσουμε μια συγγνώμη. Σας υποσχεθήκαμε να ασχοληθούμε με physics σ’ αυτό το τεύχος, αλλά τελικά θα χρειαστεί να το αναβάλουμε για κάποιο επόμενο (ίσως και για το επόμενο). Για το λόγο αυτό, η θεωρία του παρόντος άρθρου θα είναι ελάχιστη.

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

Στο παράθυρο Hierarchy του Unity επιλέγουμε το αντικείμενο Spawner και πατάμε [Ctrl+D], για να δημιουργήσουμε ένα αντίγραφό του. Το τοποθετούμε στο σημείο (-5, 0.5, -5.5), θέτοντας τις αντίστοιχες τιμές στα πεδία Position του Transform του. Επαναλαμβάνουμε τη διαδικασία δημιουργώντας άλλα επτά αντίγραφα. Οι θέσεις των spawners στην πίστα, με πρώτον αυτόν που βρίσκεται πιο κοντά στη σφαίρα του παίχτη (και προς τα πάνω, όπως βλέπουμε την πίστα) είναι οι ακόλουθες:

  1. (5, 0.5, -7.5)
  2. (-5, 0.5, -5.5)
  3. (5, 0.5, -1,5)
  4. (-5, 0.5, 0.5)
  5. (5, 0.5, 2.5)
  6. (-5, 0.5, 6.5)
  7. (5, 0.5, 8.5)
  8. (-5, 0.5, 10.5)
  9. (5, 0.5, 12.5)

Λογικά θα παρατηρήσατε την αλληλουχία των αριθμών. Θα ήταν πολύ εύκολο να κάνουμε ένα script που θα τοποθετεί τους spawners αυτόματα στις θέσεις τους και τυχαία αριστερά ή δεξιά της πίστας, ώστε να δώσουμε ακόμη μια νότα τυχαιότητας στη δημιουργία της πίστας. Θ’ αφήσουμε όμως τέτοιες αλλαγές για το polishing του παιχνιδιού, στο τέλος.

Παρατηρώντας το Hierarchy, πιστεύουμε ότι οι spawners θα ήταν καλύτερα αν βρίσκονταν συμμαζεμένοι σε ένα GameObject. Θα αποφύγουμε επιπλέον το clutter του Hierarchy σε περίπτωση που ψάχνουμε να βρούμε κάτι (και δεν χρησιμοποιούμε το search).

  • Δημιουργούμε ένα νέο κενό GameObject στην πίστα, με [Ctrl+Shift+N]
  • Το ονομάζουμε “Spawners”
  • Το τοποθετούμε στο σημείο (0, 0, 0) και
  • σέρνουμε όλα τα αντικείμενα Spawner της πίστας μας σε αυτό. Τα μαρκάρουμε όλα με το [Ctrl] και τα μεταφέρουμε στο άδειο αντικείμενο, ώστε να γίνουν παιδιά του.
  • Έπειτα κάνουμε κλικ στο βελάκι αριστερά του αντικειμένου Spawners, ώστε να τα κρύψουμε. Αν ποτέ τα χρειαστούμε, από αυτό το βελάκι μπορούμε να τα εμφανίσουμε.
  • Επιλέγοντας όλα τα αριστερά αντικείμενα Spawner από τη σκηνή (ή από το Hierarchy), στο component Spawner που εμφανίζεται θέτουμε το Speed=1 στον Inspector.
  • Αντίστοιχα, σε όλα τα αντικείμενα στα δεξιά της πίστας, το Speed πρέπει να είναι ίσο με -1.

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

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

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

Πίστα και διακόσμηση
Βλέποντας την πίστα, έχουμε την εντύπωση ότι είναι λίγο μονότονη. Πάμε να σπάσουμε αυτή την πρασινίλα της, προσθέτοντας μερικά διακοσμητικά props. Αν θυμόσαστε από το game design document των τριών παραγράφων στο άρθρο που ξεκινήσαμε τη δημιουργία του παιχνιδιού, είχαμε πει ότι θα χωρίσουμε τα εμπόδια σε ομάδες. Θα έχουμε κάποιες νεκρές ζώνες, χρώματος λευκού, ώστε να διευκολύνουμε το χρήστη. Στο τέλος της πίστας, προκειμένου να σηματοδοτήσουμε τον τερματισμό θα χρησιμοποιήσουμε μια ασπρόμαυρη καρό γραμμή. Σαν τη σημαία που χρησιμοποιούν στην Formula 1.

Ας ξεκινήσουμε με τις λευκές γραμμές.

  • Δημιουργούμε ένα νέο Plane με δεξί κλικ στο Hierarchy και 3D Object –> Plane.
  • Το ονομάζουμε RestZone και
  • θέτουμε Scale = (1, 1, 0.1).
  • Το τοποθετούμε στο σημείο (0, 0, -3.5).
  • Με [Ctrl+D] κάνουμε ένα αντίγραφο και το τοποθετούμε στο σημείο (0, 0, 4.5).

Αυτές είναι οι δύο νεκρές ζώνες.

Για να δημιουργήσουμε τη γραμμή του τερματισμού, θα χρειαστεί να κάνουμε το texture της. Στο Paint (!), δημιουργούμε ένα αρχείο PNG μεγέθους 128×128 pixels. Zωγραφίζουμε ένα μαύρο τετράγωνο 64×64 pixels πάνω αριστερά κι άλλο ένα κάτω δεξιά του κέντρου της εικόνας. Το αποθηκεύουμε ως checkered.png στον φάκελο “Assets/Materials” του project μας. Με το που θα γυρίσουμε στον Editor, η Unity θα έχει ήδη διαβάσει το texture, θα το έχει εισαγάγει στο παιχνίδι μας και θα μπορούμε να το δούμε στο παράθυρο Project.

Είδατε που το Paint τελικά χρησιμοποιείται *και* για game development;

Είδατε που το Paint τελικά χρησιμοποιείται και για game development;

Η μηχανή βλέπει το αρχείο ως Texture, αλλά για να το χρησιμοποιήσουμε σε κάποιο μοντέλο πρέπει να δημιουργήσουμε ένα material μ’ αυτό.

  • Στο φάκελο Assets/Materials του project, κάνοντας δεξί κλικ κι επιλέγουμε Create –> Material.
  • Το νέο meterial τ’ ονομάζουμε “FinishLine” και
  • σέρνουμε το texture “checkered” από το Project στον Inspector, στο κουτί που βρίσκεται αριστερά της ιδιότητας Albedo του νέου material που έχουμε δημιουργήσει.

Το material είναι έτοιμο, οπότε πάμε να δημιουργήσουμε τη γραμμή τερματισμού.

  • Στο Hierarchy κάνουμε ακόμα ένα αντίγραφο του RestZone (με [Ctrl+D]).
  • Το ονομάζουμε FinishLine και
  • το τοποθετούμε στο σημείο (0, 0, 14.5).
  • Βρίσκουμε το component με όνομα Mesh Rendered, επεκτείνουμε τη λίστα “Materials” και θέτουμε σαν “Element 0” το material FinishLine από τα assets μας.

Η πίστα δεν χωράει σε μήκος όλα τα στοιχεία που της βάλαμε, οπότε πρέπει να κάνουμε κάποιες αλλαγές και σε αυτή. Με επιλεγμένο το αντικείμενο Floor στο Hierarchy, αλλάζουμε το Position του σε (0, 0, 2.5) και το Scale του σε (1, 1, 2.5).
Έτσι, η πίστα μας θα έχει αρκετό μήκος ώστε να καλύψει όλη την απόσταση που πρέπει να διανύσει ο χρήστης. Σε αυτό το σημείο η οθόνη σας πρέπει να φαίνεται όπως δείχνει η εικόνα παρακάτω.

Z-Fighting, όταν τα textures προσπαθούν να αλληλεπικαλυφθούν.

Z-Fighting, όταν τα textures προσπαθούν να αλληλεπικαλυφθούν.

Τι θα γίνει render τελικά;
Είναι φανερό ότι κάτι δεν πάει καλά με τα textures των γραμμών που έχουμε βάλει στην πίστα. Το φαινόμενο αυτό ονομάζεται Z-Fighting και παρουσιάζεται όταν δύο επιφάνειες έχουν παρόμοιες τιμές στο z-buffer.

Σε μια σκηνή (κι όχι μόνο) είναι λογικό κάποια αντικείμενα να βρίσκονται μπροστά ή πίσω από κάποια άλλα. Η φυσιολογία του ματιού του επιτρέπει να διακρίνει ό,τι βρίσκεται πιο μπροστά (ξεχάστε τα αντικείμενα που είναι διάφανα ή ημιδιάφανα για λίγο). Ο αλγόριθμος του rendering της κάρτας γραφικών πάνω κάτω κάνει τη δουλειά του ματιού μας. Πρέπει να ζωγραφίσει μια ολοκληρωμένη εικόνα στην οθόνη μας, όπως θα τη βλέπαμε και με τα μάτια μας αν ήταν πραγματικότητα. Από τη φύση του, ο αλγόριθμος δεν κάνει διακρίσεις στο τι πρέπει να προβληθεί ή όχι. Όποιο αντικείμενο βρίσκεται μέσα στο viewport πρέπει να προβληθεί, αλλά αυτό δεν σημαίνει ότι πρέπει να είναι κι ορατό από το χρήστη.

Όταν ένα αντικείμενο πρέπει να προβληθεί στην οθόνη μας και μια και η εικόνα που βλέπουμε είναι δύο διαστάσεων, το βάθος κάθε pixel (δηλαδή η τρίτη διάσταση) που ανήκει στο αντικείμενο αυτό, αποθηκεύεται σε ένα προσωρινό πίνακα δύο διαστάσεων. Ο πίνακας αυτός ονομάζεται z-buffer ή depth buffer. Όταν λοιπόν περισσότερα από ένα αντικείμενα πρέπει να προβληθούν στο ίδιο pixel, o αλγόριθμος του rendering πρέπει να έχει ένα τρόπο να αποφασίσει ποιο από αυτά θα είναι ορατό και ποιο δεν θα είναι. Προς την κατεύθυνση αυτή βοηθάει το z-buffer.

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

Όταν δύο pixels είναι εξαιρετικά κοντά το ένα με το άλλο και μια και η ακρίβεια που μπορούν να έχουν οι αριθμοί κινητής υποδιαστολής (floating point numbers ή floats) στους υπολογιστές είναι πεπερασμένη, υπάρχει πιθανότητα το ένα pixel να ανήκει στην μία επιφάνεια, αλλά το διπλανό του να ανήκει στην άλλη. Ως αποτέλεσμα, το ένα pixel είναι πράσινο ενώ το διπλανό του λευκό. Αν κινήσετε το viewport της σκηνής με το ποντίκι, θα δείτε ότι οι επιφάνειες τρεμοπαίζουν αλλάζοντας χρώμα συνέχεια. Αυτό συμβαίνει μια κι ο αλγόριθμος πέφτει στη λούπα των λαθών αυτών. Το pixel που ανήκει στη μία επιφάνεια είναι πιο πάνω από το αντίστοιχο της άλλης (αφού κι ο float του βάθους έχει υπολογιστεί μ’ αυτό τον τρόπο), ενώ το διπλανό του είναι πιο κάτω (οπότε πρέπει να έχει τα χρώματα του pixel της δεύτερης), αφού υπάρχει λάθος στον float του βάθους του.

Το συγκεκριμένο θέμα αποτελεί κυρίως πρόβλημα της μηχανής και του τρόπου που κάνει render. Μια και είναι πέρα από τις δυνατότητές μας να το επιλύσουμε στη ρίζα του, θα το ξεπεράσουμε με έναν απλό τρόπο. Θα τοποθετήσουμε λίγο πιο ψηλά τα planes των γραμμών, ώστε να είναι ελάχιστα πιο πάνω από το plane της πίστας, αποφεύγοντας έτσι το πρόβλημα.

Η λύση του προβλήματος
Επιλέξτε τις δύο λευκές και τη μία καρό γραμμές κι αλλάξτε τη θέση τους στον άξονα y, από 0 σε 0.001. Παρατηρήστε ότι αν προσπαθήσετε να τις τοποθετήσετε στο 0.0001, το πρόβλημα εξακολουθεί να υπάρχει. Αυτό έχει να κάνει με το εύρος του σφάλματος που συμβαίνει κατά το rendering. Μετά από αυτή την αλλαγή, οι γραμμές θα πρέπει να είναι ολόκληρες και να καλύπτουν την πίστα από άκρη σε άκρη.

Από τη στιγμή που τώρα φαίνεται καθαρά, σίγουρα θα το παρατηρήσατε. Το texture της γραμμής τερματισμού τεντώνεται ώστε να καλύψει όλη την επιφάνειά της, με αποτέλεσμα να μη φαίνεται όμορφο. Προφανώς όμως θέλουμε να το κάνουμε να φαίνεται σωστά. Πρέπει να ρυθμίσουμε το tiling του material. Το tiling του material υποδηλώνει αν το ίδιο texture θα επαναληφθεί περισσότερες από μία φορές (και σε ποιον άξονα) με σκοπό να καλύψει την επιφάνεια που του έχει ανατεθεί.

Πηγαίνοντας στον Inspector έχοντας επιλέξει το material FinishLine από το παράθυρο του project, στο πεδίο Tiling θέτουμε X=8. Αυτό σημαίνει ότι το texture θα επαναληφθεί 8 φορές στον άξονα Χ. Πλέον, βλέπουμε ότι το καρό της γραμμής τερματισμού εμφανίζεται όπως πρέπει.

Κάποιες λεπτομέρειες
Τρέχοντας το παιχνίδι βλέπουμε ότι τα άλματα της σφαίρας του παίχτη είναι πολύ μικρά. Μπορούμε αν θέλουμε να μικρύνουμε όλη την πίστα και ν’ αλλάξουμε όλες τις θέσεις από τους spawners, αλλά θα ήταν απλούστερο να αλλάξουμε το Jump Distance του Player script, συγκεκριμένα από 2 σε 4. Κι επειδή πρέπει να πιάσουμε και λίγο κώδικα, για να μην τον ξεχνάμε, πάμε να κάνουμε μερικές αλλαγές στην κίνηση της σφαίρας.

Αν προχωρήσετε τη σφαίρα αρκετά στην πίστα, θα παρατηρήσετε ότι δεν πέφτει ακριβώς πάνω στο κέντρο της γραμμής της πίστας. Αυτό γίνεται και πάλι λόγω rounding errors στον κώδικά μας. Υπολογίζουμε το πόσο μπροστά πρέπει να μετακινηθεί η σφαίρα με βάση τα δευτερόλεπτα που περνάνε από την εκτέλεση του άλματος. Το αποτέλεσμα είναι η σφαίρα να μην πέφτει ακριβώς στην τροχιά που πρέπει, αλλά να είναι λίγο πιο πίσω. Όσο πιο μακριά στην πίστα πάμε, τόσο περισσότερο γίνεται αντιληπτό το φαινόμενο.

Βλέπουμε ότι η σφαίρα 'αποκτά' σφάλμα σε κάθε της άλμα και, λειτουργώντας αθροιστικά, το σφάλμα γίνεται πιο αντιληπτό όσο απομακρυνόμαστε από την αρχή της πίστας.

Βλέπουμε ότι η σφαίρα “αποκτά” σφάλμα σε κάθε της άλμα και, λειτουργώντας αθροιστικά, το σφάλμα γίνεται πιο αντιληπτό όσο απομακρυνόμαστε από την αρχή της πίστας.

Στην περίπτωση αυτή θα διορθώσουμε το bug με μια χακεριά, μια και ξέρουμε ότι το άλμα είναι ελάχιστα ελλιπές στο τέλος της κίνησής του. Θα θέσουμε λοιπόν τη θέση της σφαίρας ίση με τις μονάδες της αυξημένες κατά 0.5, όταν ο κώδικας αποφασίζει ότι το άλμα έχει τελειώσει.

Οπότε ο κώδικας της μεθόδου handleJump του Player script πρέπει να αλλάξει, όπως φαίνεται παρακάτω:

private void handleJump()
{
	currentJumpTime += Time.deltaTime;
	float animationEval = JumpCurve.Evaluate(currentJumpTime * relativeCurveTime);
	float currentHeight = normalY + (animationEval * JumpHeight);
	float forwardMovement = jumpDirection * JumpDistance * Time.deltaTime;

	Vector3 jumpVector = new Vector3(transform.position.x, currentHeight, transform.position.z + forwardMovement);
	transform.position = jumpVector;

	if (currentJumpTime >= JumpDuration)
	{
		transform.position = new Vector3(transform.position.x, transform.position.y, Mathf.FloorToInt(transform.position.z) + 0.5f);
		jumping = false;
	}
}

Η αλλαγή που κάνουμε είναι απλή. Μόλις ο κώδικας δει ότι το άλμα έχει τελειώσει, θα διορθώσει τη θέση της σφαίρας προσθέτοντας μισή ακόμα μονάδα στις ακέραιες μονάδες της τρέχουσας θέσης του. Πλέον, η σφαίρα μετά από κάθε άλμα της θα τοποθετείται ακριβώς στο κέντρο της τροχιάς της γραμμής που περνάνε τα εμπόδια.

Ακόμα ένα θέμα με την παραπάνω εικόνα είναι και τα εμπόδια που δημιουργούνται. Όταν δημιουργείται ένα εμπόδιο μέσω της Instantiate, τοποθετείται στην ιεραρχία χωρίς να έχει κάποιο άλλο αντικείμενο ως πατέρα. Το αποτέλεσμα είναι να γεμίζει η ιεραρχία από αντικείμενα. Αφού τα αντικείμενα που πέρασαν από την πίστα δεν χρειάζονται πια, θα χρησιμοποιήσουμε collision detection ώστε να τα καταστρέψουμε από την πίστα. Μόνο που αυτό θα γίνει αφού συζητήσουμε για τη μηχανή φυσικής της Unity. Προς το παρόν θα αλλάξουμε τον κώδικα, ώστε κάθε εμπόδιο που εμφανίζεται στην πίστα να μπαίνει αυτόματα στο Hierarchy, ως παιδί της γεννήτριας που το δημιούργησε. Αλλάζουμε τον κώδικα της μεθόδου spawnObstacle του αρχείου Spawner.cs:

private void spawnObstacle()
{
	Obstacle.Instantiate(new InitializationData()
	{
	Position = transform.position,
	Rotation = transform.rotation,
	Prefab = ObstaclePrefab
	}, WidthModifier.Random(), Speed).transform.SetParent(transform);
}

Κι εδώ απλή είν’ η αλλαγή. Μόλις δημιουργήσουμε το εμπόδιο, ως parent του transform του θέτουμε το transform του αντικειμένου που τρέχει το script (δηλαδή του Spawner).

Σαν μια τελευταία πινελιά καλλωπισμού του Hierarchy του project, σέρνουμε τα δύο RestZones και το FinishLine στο αντικείμενο Floor, ώστε να γίνουν παιδιά του. Στο κάτω κάτω της γραφής, είναι αντικείμενα του δαπέδου και είναι λογικό να υπάρχουν σε αυτό.

Στο παρόν άρθρο, λοιπόν, εργαστήκαμε πάνω στην πίστα. Αρχικά δημιουργήσαμε τις γεννήτριες εμποδίων και τελειοποιήσαμε τις θέσεις τους πάνω στην πίστα. Ομορφύναμε την πίστα και ταυτόχρονα σπάσαμε τη μονοτονία του πράσινου χρώματός της, προσθέτοντας τις λευκές νεκρές ζώνες και τη γραμμή τερματισμού. Αντιμετωπίσαμε το πρόβλημα που είχε η κίνηση της σφαίρας του παίκτη, αλλά και το πρόβλημα του z-fighting των textures που μας παρουσιάστηκε. Τέλος, καλυτερεύσαμε την ιεραρχία των αντικειμένων στην πίστα μας.

Όπως πάντα, μπορείτε να βρείτε την τελευταία έκδοση του κώδικα στο https://github.com/ikromm/project-sphere. Σας παροτρύνουμε να υποβάλετε τυχόν απορίες αλλά και να προτείνετε βελτιώσεις, στο site του περιοδικού.

Leave a Reply

You must be logged in to post a comment.

Σύνδεση

Αρχείο δημοσιεύσεων