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

Καμπύλες Bézier

Στο προηγούμενο άρθρο της σειράς μας περί game development, μιλήσαμε για το game loop και τη σημασία του στις σημερινές μηχανές δημιουργίας παιχνιδιών, ενώ συνεχίσαμε με τον προγραμματισμό της σφαίρας που κινεί ο παίκτης στο παιχνίδι μας. Στο παρόν άρθρο καταπιανόμαστε με τις καμπύλες Bézier και προσθέτουμε κινούμενα εμπόδια.

Για να ζωγραφίσουμε κάτι σε δύο διαστάσεις, συνήθως χρησιμοποιούμε γραμμές. Οι γραμμές κατά βάση κατηγοριοποιούνται σε ευθείες και σε καμπύλες. (Σ.τ.Ε. Ένας Μαθηματικός θα σημείωνε ότι η ευθεία είναι απλά μια ειδική περίπτωση καμπύλης, αλλά ποιος δίνει σημασία στους Μαθηματικούς;) Οι ευθείες γραμμές είναι εύκολο να γίνουν, είτε από εμάς είτε από τους υπολογιστές. Για τη δημιουργία τους απλά χρειαζόμαστε το σημείο που ξεκινούν και το σημείο που τελειώνουν. (Σ.τ.Ε. Επομένως δεν μιλάμε για ευθείες, αλλά για ευθύγραμμα τμήματα. Εντάξει, εντάξει, υποσχόμαστε να μην επέμβουμε ξανά.) Οι καμπύλες, όμως, είναι αρκετά πιο πολύπλοκες — τουλάχιστον για τους υπολογιστές. Ενώ μπορούμε να ζωγραφίζουμε καμπύλες εξαιρετικά εύκολα με το χέρι, οι υπολογιστές θα λέγαμε ότι είναι λίγο προβληματικοί στον τομέα αυτό.

Οι αρτιστικές ικανότητες των μηχανών
Για να σχεδιαστεί μια γραμμή από έναν υπολογιστή, το μηχάνημα –ή ακριβέστερα το πρόγραμμα– πρέπει να γνωρίζει τη μαθηματική συνάρτηση που ορίζει τα σημεία απ’ όπου περνάει η γραμμή αυτή, ώστε να τα παρουσιάσει (με κάποιο χρώμα ίσως) στην οθόνη μας. Αυτό συμβαίνει και για τους δύο τύπους γραμμών. Όταν πρόκειται για ευθείες γραμμές, οι συναρτήσεις είναι εύκολες. Μια και οι συναρτήσεις είναι εύκολες, αντίστοιχα εύκολος είναι και ο υπολογισμός του αποτελέσματός τους. Χρειαζόμαστε μαθηματικές συναρτήσεις που να μπορούν να σχηματίζουν “όμορφες” καμπύλες, αλλά που παράλληλα είναι και “γρήγορες” στον υπολογισμό τους. Υπάρχουν αρκετές συναρτήσεις που μπορούν να χρησιμοποιηθούν για τη δημιουργία καμπυλών, αλλά μια ομάδα συναρτήσεων απέκτησε μεγάλη δημοτικότητα και πλέον χρησιμοποιούνται πάνω κάτω σε οτιδήποτε χρειάζεται να ζωγραφίσει καμπύλες γραμμές. Το όνομα αυτών; Καμπύλες Bézier.

Πήραν το όνομά τους από τον Γάλλο Pierre Bézier που τις παρουσίασε στις αρχές τις δεκαετίας του ’60, ως καμπύλες για δουλειές σχεδιασμού. Η αλήθεια είναι ότι υπάρχει μια σύγχυση για το ποιος πραγματικά ήταν ο πρώτος που δημιούργησε συναρτήσεις για τις καμπύλες αυτές, αφού ο Casteljau τις είχε επινοήσει νωρίτερα αλλά δεν δημοσίευσε την έρευνά του. Σε κάθε περίπτωση, αυτό που ενδεχομένως να μας ενδιαφέρει περισσότερο είναι ότι μπορούμε να συνδέσουμε περισσότερες από μία καμπύλες μεταξύ τους κάνοντάς τες να φαίνονται σαν μία.

Οι καμπύλες Bézier παίρνουν ονόματα ανάλογα με το βαθμό των συναρτήσεων που χρησιμοποιούνται για να τις απεικονίσουν.

Οι καμπύλες Bézier παίρνουν ονόματα ανάλογα με το βαθμό των συναρτήσεων που χρησιμοποιούνται για να τις απεικονίσουν.

Οι καμπύλες Bézier είναι αποτέλεσμα γραμμικών παρεμβολών. Ουσιαστικά μιλάμε για τη διαδικασία επιλογής ενός σημείου, το οποίο βρίσκεται ανάμεσα σε δύο άλλα σημεία. Όπως όλες οι γραμμές, έτσι και οι καμπύλες Bézier έχουν ένα σημείο αρχής και ένα σημείο τέλους. Η δε καμπυλότητά τους επηρεάζεται από ένα ή περισσότερα ενδιάμεσα σημεία ελέγχου.

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

q0 = CalculateBezierPoint(0, p0, p1, p2, p3);
for(int i = 1; i <= SEGMENT_COUNT; i++)
{
	t = i / (float) SEGMENT_COUNT;
	q1 = CalculateBezierPoint(t, p0, p1, p2, p3);
	DrawLine(q0, q1);
	q0 = q1;
}

Vector3 CalculateBezierPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3)
{
	float u = 1 – t;
	float tt = t*t;
	float uu = u*u;
	float uuu = uu * u;
	float ttt = tt * t;
	Vector3 p = uuu * p0; //first term
	p += 3 * uu * t * p1; //second term
	p += 3 * u * tt * p2; //third term
	p += ttt * p3; //fourth term

	return p;
}

Ας κάνουμε μερικά σχόλια επ’ αυτού. Για να ζωγραφίσουμε μια καμπύλη θα χρειαστούμε 4 σημεία. Το σημείο αρχής της καμπύλης, p0, τα δύο ενδιάμεσα σημεία p1 και p2 (αυτά που ορίζουν την καμπυλότητα), καθώς και το σημείο τέλους της καμπύλης, p3. Ο αλγόριθμος θα ζωγραφίσει πολλές μικρές γραμμές μεταξύ διαδοχικών σημείων της καμπύλης αφού πρώτα τα υπολογίσει. Το πλήθος των επαναλήψεων – και κατ’ επέκταση το πλήθος των γραμμών – ορίζεται από τη μεταβλητή SEGMENT_COUNT. Όσο περισσότερο μεγαλώσουμε την τιμή της, τόσο περισσότερες επαναλήψεις θα έχουμε. Το αποτέλεσμα θα είναι μια πιο ομαλή καμπύλη.

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

Όπως ενδέχεται ήδη να καταλάβατε, ο κώδικας παραπάνω μπορεί να απεικονίσει μια κυβική καμπύλη Bézier σε τρισδιάστατο χώρο. Αυτό το υποδηλώνει ο τύπος Vector3 που χρησιμοποιήσαμε για τα σημεία, ο οποίος περιλαμβάνει τιμές για τον προσδιορισμό ενός σημείου σε ένα τρισδιάστατο χώρο (x, y, z). Αν θέλουμε να περιορίσουμε τις καμπύλες στους δύο μόνο άξονες, δηλαδή να ζωγραφίσουμε σε δύο διαστάσεις, το μόνο που θα χρειαστεί να κάνουμε είναι αντί του τύπου Vector3 να χρησιμοποιήσουμε τον τύπο Vector2. Ο τύπος αυτός περιλαμβάνει τιμές για τη θέση ενός σημείου σε δύο άξονες (x, y). Τα υπόλοιπα θα τα κάνει ο επεξεργαστής για εμάς.

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

Μια και τα εμπόδια θα εμφανίζονται μόνα τους (μέσω script) στην πίστα, θα χρειαστεί να είναι αποθηκευμένα ως ένα prefab. Για ευκολία το prefab θα βρίσκεται στον φάκελο Resources, πράγμα που θα μας επιτρέψει να φορτώσουμε και να χρησιμοποιήσουμε αντικείμενα που δεν υπάρχουν ήδη στην σκηνή μας.

Ξεκινάμε δημιουργώντας το εμπόδιο και δίνοντάς του ένα μαύρο χρώμα.

  • Έχουμε τη σκηνή Game.unity ανοιχτή και πάμε GameObject –> 3D Object –> Cube.
  • Ονομάζουμε το νέο αντικείμενο “Obstacle”, μια και θα αντιπροσωπεύει τα εμπόδια της πίστας.
  • Τοποθετούμε τον κύβο στη θέση (0, 1, 0). Στον Inspector, θέτουμε τις αντίστοιχες τιμές στα πεδία της τιμής του Position, στο Transform Component του κύβου.
  • Κάνουμε δεξί κλικ στο φάκελο Materials, στο παράθυρο Project, επιλέγουμε Create –> Material για να δημιουργήσουμε ένα νέο material, του δίνουμε και το όνομα “Obstacle”.
  • Στον Inspector, θέτουμε το χρώμα του Albedo σε μαύρο (#000000FF).
  • Αναθέτουμε το material που μόλις δημιουργήσαμε στον κύβο, κάνοντάς το drag and drop από το παράθυρο Project στον κύβο που φαίνεται στην σκηνή ή στο αντικείμενο “Obstacle”, που φαίνεται στο παράθυρο Hierarchy.
  • Έτοιμοι να γράψουμε το script των εμποδίων. Πηγαίνουμε στο φάκελο Scripts, στο παράθυρο Project, με δεξί κλικ δημιουργούμε ένα νέο αρχείο C# Script με όνομα Obstacle.cs, προσθέτουμε τον παρακάτω κώδικα.
    using UnityEngine;
    
    namespace Deltahacker
    {
        public class Obstacle : MonoBehaviour
        {
            public float WidthModifier = 1;
            public float Speed;
     
            public void Start()
            {
                transform.localScale = new Vector3(WidthModifier, 1, 1);
            }
     
            public void Update()
            {
                transform.position += new Vector3(Speed * Time.deltaTime, 0, 0);
            }
     
            public static GameObject Instantiate(InitializationData data, float scale, float speed)
            {
                GameObject instance = (GameObject)Instantiate(data.Prefab, data.Position, data.Rotation);
     
                Obstacle script = instance.GetComponent<Obstacle>();
                script.Speed = speed;
                script.WidthModifier = scale;
     
                return instance;
            }
        }
    
        public struct InitializationData
        {
            public GameObject Prefab;
            public Vector3 Position;
            public Quaternion Rotation;
        }
    }
    

    Τα εμπόδια στο παιχνίδι μας είναι απλά κουτιά, χωρίς ιδιαίτερους μηχανισμούς. Στον παραπάνω κώδικα βλέπουμε ότι χρειαζόμαστε μια μεταβλητή που θα περιέχει την τιμή της ταχύτητας του εμποδίου, καθώς και άλλη μία που θα περιέχει την τιμή του μήκους του εμποδίου. Η ταχύτητα χρησιμοποιείται στην μέθοδο Update για να μεταθέσει τον κύβο αριστερά ή δεξιά (εξαρτάται από το πρόσημο της ταχύτητας) ανάλογα με την ταχύτητα εκτέλεσης του κώδικα του παιχνιδιού (εξ’ ου και η χρήση της Time.deltaTime). Η τιμή του μήκους χρησιμοποιείται ουσιαστικά για την αρχικοποίηση του αντικειμένου στη μέθοδο Start. Δεν μπορούμε να τη θέσουμε στην Awake, μιας και η μέθοδος αυτή θα κληθεί αμέσως μετά την Instantiate οπότε δεν θα έχει τεθεί η τιμή της ακόμα κι έτσι το μήκος του εμποδίου θα γίνει μηδενικό.

    Η static μέθοδος Instantiate είναι απλά ένας wrapper της αντίστοιχης μεθόδου του API της μηχανής. Θα μπορούσε να παραλειφθεί εντελώς σε αυτή την κλάση και να χρησιμοποιηθεί κατευθείαν στο σημείο που θα γίνει η κλήση της, αλλά βρίσκουμε τον κώδικα πιο τακτοποιημένο με αυτόν τον τρόπο. Η μέθοδος παίρνει σαν παραμέτρους ένα struct τύπου InitializationData, καθώς και δύο μεταβλητές τύπου float: μία για την ταχύτητα του εμποδίου και άλλη μία για το μέγεθός του. Μέσα σε αυτή χρησιμοποιούμε τη μέθοδο Instantiate της Unity, προκειμένου να δημιουργήσουμε το αντίγραφο. Με τη GetComponent θέτουμε την ταχύτητά του και το μέγεθος, στο script που έχουμε προσκολλήσει στο αντικείμενο.

    Ας πούμε δυο λόγια για τη μέθοδο Object.Instatiate (του API της Unity). Η μέθοδος χρησιμοποιείται για να δημιουργήσουμε ένα αντίγραφο ενός αντικειμένου. Μαζί με το αντικείμενο δημιουργούνται κι όλα τα αντικείμενα-παιδιά, που βρίσκονται κάτω από αυτό, διατηρώντας όλες τις ιδιότητές τους. Η μέθοδος είναι επίπονη για τον επεξεργαστή και πρέπει να αποφεύγεται η χρήση της για μεγάλο αριθμό αντικειμένων ταυτόχρονα. Αν χρειαστεί να γίνει κάτι τέτοιο προτείνεται η χρήση object pools, δηλαδή πινάκων (ή λιστών) από GameObjects, τα οποία αρχικοποιούνται κατά τη φόρτωση της εκάστοτε πίστας και πριν δοθεί ο χειρισμός στον παίχτη, δηλαδή στη μέθοδο Awake.

  • Με επιλεγμένο το Obstacle στο παράθυρο Hierarchy, αναθέτουμε το νέο script στο GameObject Obstacle κάνοντάς το drag and drop από το παράθυρο Project, στον Inspector. Σε αυτό το σημείο μπορούμε να ελέγξουμε την κίνηση του εμποδίου, τρέχοντας την σκηνή μας κι αλλάζοντας την τιμή του πεδίου Speed του εμποδίου.

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

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

  • Στο παράθυρο project δημιουργούμε ένα νέο φάκελο με όνομα “Resources”.
  • Τον επιλέγουμε (σαν να θέλαμε να δούμε τα περιεχόμενά του) και σέρνουμε το GameObject Obstacle από το παράθυρο Hierarchy, στο παράθυρο project.
  • Όταν δημιουργηθεί, το σβήνουμε από την σκηνή επιλέγοντάς το και πατώντας [Delete].

Γεννήτριες εμποδίων
Ας περάσουμε στη δημιουργία των αντικειμένων που θα είναι υπεύθυνα για την εμφάνιση εμποδίων στην πίστα. Τα αντικείμενα αυτά θα είναι τοποθετημένα στο μέσο των νοητών γραμμών της πίστας (στις θέσεις που μπορεί να πηδάει η μπάλα), ώστε να μπορούμε ανά πάσα στιγμή να τα κάνουμε τα εμπόδιά τους να κινούνται αριστερά ή δεξιά. Στον φάκελο Scripts δημιουργούμε ένα νέο αρχείο κώδικα, με όνομα Spawner.cs. Σε αυτό προσθέτουμε τον ακόλουθο κώδικα:

using UnityEngine;

namespace Deltahacker
{
    public class Spawner : MonoBehaviour
    {
        public GameObject ObstaclePrefab;
        public float Speed;
 
        public float Frequency;
        public FloatRange WidthModifier;
 
        private float lastSpawn;
 
        public void Update()
        {
            if (Time.time >= lastSpawn + Frequency)
            {
                lastSpawn = Time.time;
                spawnObstacle();
            }
        }
 
        private void spawnObstacle()
        {
            Obstacle.Instantiate(new InitializationData()
            {
                Position = transform.position,
                Rotation = transform.rotation,
                Prefab = ObstaclePrefab
            }, WidthModifier.Random(), Speed);
        }
    }

    [System.Serializable]
    public struct FloatRange
    {
        public float Min;
        public float Max;
 
        public float Random()
        {
            return UnityEngine.Random.Range(Min, Max);
        }
    }
}

Αρχικά δηλώνουμε κάποια πεδία μεταβλητών που θα χρησιμοποιήσουμε. Το ObstaclePrefab δηλώνει το προκατασκευασμένο αντικείμενο που χρησιμοποιούμε για τα εμπόδια. To Speed δηλώνει την ταχύτητα που θα περάσουμε στο εμπόδιο κατά την κατασκευή του. To Frequency δηλώνει τη συχνότητα που θα εμφανίζονται τα εμπόδια και, τέλος, το struct WidthModifier δηλώνει το εύρος των μηκών των εμποδίων. Δεύτερο struct για σήμερα, οπότε ας εξηγήσουμε λίγο παραπάνω την κατάσταση.

Στη C# υπάρχει τρόπος να κάνουμε μια τιμή να περιλαμβάνει ένα μικρό αριθμό από μεταβλητές, συχνά συσχετιζόμενες. Αυτό επιτυγχάνεται με τη χρήση ενός struct. Πολύ κοινή τους χρήση είναι, π.χ., για τα συστήματα συντεταγμένων. Το Vector3 που χρησιμοποιήσαμε στον κώδικα των εμποδίων, είναι ένα struct που μας το δίνει έτοιμο η Unity και περιλαμβάνει τρία floats — ένα για κάθε άξονα (x, y και z). Μπορεί να μοιάζουν πολύ με κλάσεις, αλλά δεν είναι. Αν και δεν πρόκειται τόσο για θέμα του προγραμματισμού βιντεοπαιχνιδιών όσο της C#, ενδεχομένως να τα μελετήσουμε περισσότερο σε επόμενο άρθρο.

Στο παραπάνω struct λοιπόν έχουμε ένα ζευγάρι από floats που υποδηλώνουν την ελάχιστη και τη μέγιστη τιμή που θα χρησιμοποιούμε για να τροποποιήσουμε το μήκος των εμποδίων. Μέσα στο ίδιο το struct, για ευκολία, δηλώνουμε μια μέθοδο Random, η οποία χρησιμοποιώντας την κλάση Random της Unity επιστρέφει ένα ψευδοτυχαίο αριθμό στο εύρος που δηλώσαμε.

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

Επιστρέφοντας στη Unity, ώρα να δημιουργήσουμε μερικούς spawners.

  • Στο μενού GameObject επιλέγουμε Create Empty — ή πατάμε το συνδυασμό πλήκτρων [CTRL+Shift+N].
  • Ονομάζουμε το νέο αντικείμενο “Spawner”.
  • Από το παράθυρο του project σέρνουμε το αρχείο κώδικα που δημιουργήσαμε λίγο πριν στον Inspector, ώστε να το προσκολλήσουμε στον Spawner.
  • Στο νέο Component που θα δημιουργηθεί θέτουμε το Obstacle Prefab στο prefab Obstacle που έχουμε ήδη δημιουργήσει, κάνοντας κλικ στο στόχο στα δεξιά της τιμής κι επιλέγοντας το prefab από το tab Assets, στο παράθυρο που θα δημιουργηθεί.

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

  • Σέρνουμε το αντικείμενο Spawner, από το παράθυρο Hierarchy στον φάκελο Resources του παραθύρου Project. Μια και θα χρησιμοποιήσουμε το αντικείμενο που υπάρχει στη σκηνή, δεν το σβήνουμε (όπως κάναμε με το εμπόδιο) και συνεχίζουμε επιλέγοντάς το στο παράθυρο Hierarchy.
  • Στο Transform component του, θέτουμε Position = (5, 0.5, -7.5).
  • Θέτουμε Speed = -1…
  • …και Frequency = 3.
  • Επεκτείνουμε το πεδίο Width Modifier ώστε να δούμε τις τιμές του, αν δεν φαίνονται ήδη, και θέτουμε Min = 0.1, Max = 2.5.

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

Η γεννήτρια εμποδίων εν δράσει!

Η γεννήτρια εμποδίων εν δράσει!

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

Όπως πάντα, μπορείτε να βρείτε την τελευταία έκδοση του κώδικα στο GitHub, ενώ περιμένουμε και τις όποιες απορίες σας από το site του περιοδικού. Στο επόμενο τεύχος θα κάνουμε τη σφαίρα του παίκτη να συγκρούεται με τα εμπόδια, χρησιμοποιώντας την ενσωματωμένη μηχανή physics που μας προσφέρει η Unity.

Leave a Reply

You must be logged in to post a comment.

Σύνδεση

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