Εργασία 1
- Ανακοινώθηκε: 23/3/2022
- Προθεσμία παράδοσης:
17/4/202225/4/2022 23:59 - 16% του συνολικού βαθμού στο μάθημα
- Repository: https://github.com/chatziko-k08/2022-project-1
- Demo: https://k08.chatzi.org/games/2022/solution/game.html
Σημαντικό: διαβάστε τις οδηγίες (κοινές για όλες τις εργασίες). Περιέχουν μεταξύ άλλων και πληροφορίες για τη χρήση του git, η οποία είναι ολόιδια με τα εργαστήρια.
Στην εργασία αυτή θα χρησιμοποιήσετε έτοιμη υλοποίηση για τους περισσότερους ΑΤΔ που είδαμε
στο μάθημα, η οποία παρέχεται από τη βιβλιοθήκη k08.a
στο git repository της εργασίας.
Τον κώδικα τον κάνουμε compile όπως έχουμε δει στο μάθημα.
Στα Windows χρειάζεστε επιπλέον το εργαλείο VcXsrv, δείτε τις οδηγίες για WSL.
Γενικά
Στην εργασία αυτή καλείστε να υλοποιήσετε ένα video game, με γραφικό interface και interactive gameplay, εμπνευσμένο από το κλασικό River Raid (emulator). Στο παιχνίδι αυτό ένα μαχητικό αεροσκάφος κινείται ανάμεσα στις όχθες ενός ποταμού, στον οποίο βρίσκονται 3 ειδών αντικείμενα:
- Πολεμικά πλοία, που κινούνται και το αεροσκάφος πρέπει να αποφύγει ή να ανατινάξει.
- Ελικόπτερα, που έχουν παρόμοια συμπεριφορά με τα πλοία, αλλά κινούνται πιο γρήγορα.
- Γέφυρες, τις οποίες το αεροσκάφος πρέπει να ανατινάξει για να περάσει.
Η ανατίναξη των αντικειμένων γίνεται εκτοξεύοντας πυραύλους, οι οποίοι πρέπει να βρουν στόχο πριν μπορέσει να εκτοξευτεί ο επόμενος. Το αεροσκάφος καταρρίπτεται αν έρθει σε σύγκρουση είτε με κάποιον εχθρό, είτε με τις όχθες του ποταμού. Σκοπός του παιχνιδιού είναι το αεροσκάφος να καλύψει όσο το δυνατόν μεγαλύτερη απόσταση, κερδίζοντας πόντους για κάθε ανατίναξη.
Για την υλοποίηση του γραφικού interface του παιχνιδιού θα χρησιμοποιήσετε τη βιβλιοθήκη raylib η οποία περιέχεται στο repository της εργασίας. Η βιβλιοθήκη υποστηρίζει όλα τα βασικά λειτουργικά συστήματα, αλλά και δίνει τη δυνατότητα να κάνουμε compile το παιχνίδι σε μορφή που μπορεί να ενσωματωθεί σε μια web σελίδα.
Είναι παράδειγμα υλοποίησης του παιχνιδιού υπάρχει εδώ.
Modules και information hiding.
Η διαχείριση της κατάστασης του παιχνιδιού γίνεται από το module state.h
,
που θα βρείτε στο repository της εργασίας:
include/state.h
: το interfacemodules/state.c
: η υλοποίηση που θα φτιάξετε
Σε ένα τέτοιο project οφείλουμε να διαχωρίσουμε τον κώδικα που διαχειρίζεται
την κατάσταση του παιχνιδιού (module state.h
), από τον κώδικα που
διαχειρίζεται το interface (module interface.h
, θα το φτιάξουμε αργότερα).
Για το λόγο αυτό, κάθε module θα πρέπει να εμφανίζει στο χρήστη μόνο τις
πληροφορίες που είναι απαραίτητες, και όχι πληροφορίες που αφορούν την
εσωτερική λειτουργία του. Οι “δημόσιες” αυτές πληροφορίες βρίσκονται στο
state.h
, ενώ πληροφορίες που αφορούν την υλοποίηση βρίσκονται μέσα στο
state.c
.
Game example.
Ένα πολύ απλό παράδειγμα παιχνιδιού υπάρχει στο directory
programs/game_example
στο repository της εργασίας. Συστήνεται ισχυρά να
μελετήσετε τη δομή και τον κώδικα του παραδείγματος πριν ξεκινήσετε την
εργασία.
Άσκηση 1 (15%)
Στο αρχείο modules/state.c
είναι ήδη υλοποιημένη
η συνάρτηση state_create
η οποία δημιουργεί την αρχική κατάσταση
state
του παιχνιδιού. Η κατάσταση state
περιλαμβάνει τις θέσεις όλων των
αντικειμένων και άλλες πληροφορίες για το παιχνίδι.
Αρχικά μελετήστε τον κώδικα της state_create
και κατανοήστε τους τύπους
που χρησιμοποιούνται (State
, Object
, StateInfo
) και τα περιεχόμενα της
κατάστασης του παιχνιδιού.
Στη συνέχεια υλοποιήστε τις ακόλουθες συναρτήσεις του module state.h
:
// Επιστρέφει τις βασικές πληροφορίες του παιχνιδιού στην κατάσταση state
StateInfo state_info(State state);
// Επιστρέφει μια λίστα με όλα τα αντικείμενα του παιχνιδιού στην κατάσταση state,
// των οποίων η συντεταγμένη y είναι ανάμεσα στο y_from και y_to.
List state_objects(State state, float y_from, float y_to);
Τέλος, χρησιμοποιώντας τις συναρτήσεις αυτές, δημιουργήστε ένα unit test που
να ελέγχει την ορθότητα της state_create
. Στο αρχείο tests/state_test.c
υπάρχει η βασική δομή του test, το οποίο πρέπει να επεκτείνετε (δε χρειάζεται
πρόγραμμα που να παίρνει είσοδο από το χρήστη, μόνο το unit test). Το test
δεν χρειάζεται να είναι εξαντλητικό, αλλά να ελέγχει τα βασικά χαρακτηριστικά
της κατάστασης που δημιουργεί η state_create
, πχ τον αριθμό, τις
συντεταγμένες, κλπ, των αντικειμένων. Επίσης το test θα πρέπει να δοκιμάζει
κλήσεις της state_objects
για 2-3 διαφορετικές τιμές των y_from,y_to
.
Άσκηση 2 (20%)
Συνεχίζοντας την υλοποίηση του παιχνιδιού, καλείστε να υλοποιήσετε μέρος
της τελευταίας συνάρτησης του module stats.h
:
// Ενημερώνει την κατάσταση state του παιχνιδιού μετά την πάροδο 1 frame.
// Το keys περιέχει τα πλήκτρα τα οποία ήταν πατημένα κατά το frame αυτό.
void state_update(State state, KeyState keys);
Η κατάσταση του παιχνιδιού ενημερώνεται ανάλογα με τα πλήκτρα που είναι πατημένα στο συγκεκριμένο frame
(παράμετρος keys
) με βάση τους ακόλουθους κανόνες:
Κίνηση αεροσκάφους:
- Μετακινείται 3 pixels προς τα πάνω σε κάθε frame. Αν είναι πατημένο το πάνω ή κάτω βέλος τότε η κίνηση είναι 6 ή 2 pixels αντίστοιχα.
- Αν είναι πατημένο το αριστερό ή δεξί βέλος, μετακινείται 3 pixels αριστερά ή δεξιά.
Κίνηση εχθρών : 4 pixels (ελικόπτερα) και 3 pixels (πλοία) ανά frame στην κατεύθυνση της κίνησής τους.
Συγκρούσεις:
- Αν ο το αεροσκάφος συγκρουστεί με έδαφος, εχθρό ή γέφυρα τερματίζει το παιχνίδι.
- Αν ένας εχθρός συγκρουστεί με έδαφος, αλλάζει κατεύθυνση.
Για τις συγκρούσεις μπορείτε να χρησιμοποιήσετε (χωρίς να είναι απαραίτητο) τις συναρτήσεις
CheckCollisionRecs, CheckCollisionPointRec, ...
από τοlibraylib.h
.Εκκίνηση και διακοπή:
- Αν το παιχνίδι έχει τελειώσει και πατηθεί
enter
, τότε ξαναρχίζει από την αρχή. - Αν πατηθεί
P
το παιχνίδι μπαίνει σε pause και δεν ενημερώνεται πλέον. - Αν το παιχνίδι είναι σε pause και πατηθεί
N
τότε ενημερώνεται για μόνο 1 frame (χρήσιμο για debugging).
- Αν το παιχνίδι έχει τελειώσει και πατηθεί
Είστε ελεύθεροι να προσαρμόσετε τους κανόνες αυτούς, σε λογικά πλαίσια, ανάλογα με το interface που θα υλοποιήσετε αργότερα.
Τέλος, επεκτείνετε το tests/test_state.c
ώστε να ελέγχει τη λειτουργία της state_update
.
Δεν χρειάζεται στο test να ελέγξετε όλο το functionality της state_update,
αρκεί μόνο ο έλεγχος ότι η θέση του αεροσκάφους ενημερώνεται σωστά
ανάλογα με τα πλήκτρα που είναι πατημένα.
Δεν απαιτούνται έλεγχοι για τις συγκρούσεις ή για τα αντικείμενα της πίστας
(αλλά φυσικά μπορείτε να προσθέσετε επιπλέον ελέγχους αν το επιθυμείτε).
Άσκηση 3 (15%)
Στην άσκηση αυτή καλείστε να ολοκληρώσετε τη συνάρτηση state_update
που υλοποιήσατε
στην προηγούμενη άσκηση, προσθέτωντας τη λειτουργία των πυραύλων.
Πατώντας space δημιουργείται ένας πύραυλος και αποθηκεύεται στο
info.missile
. Ο πύραυλος είναι κεντραρισμένος στην οθόνη, με αρχική θέση ακριβώς πάνω από το αεροσκάφος.Όσο ο πύραυλος παραμένει ενεργός, δεν μπορεί να δημιουργηθεί νέος.
Σε κάθε frame ο πύραυλος μετακινείται 10 pixels προς τα πάνω.
Αν ο πύραυλος συγκρουστεί με έδαφος, ή αν φτάσει σε μεγάλη απόσταση (μεγαλύτερη της μίας οθόνης) από το αεροσκάφος, καταστρέφεται (το
info.missile
γίνεταιNULL
).Αν ο πύραυλος συγκρουστεί με ελικόπτερο, πλοίο ή γέφυρα, τότε καταστρέφεται και ο πύραυλος αλλά και το αντίστοιχο αντικείμενο (αφαιρείται από τη λίστα
objects
). Επιπλέον προστίθενται 10 πόντοι στο σκορ.
Επιπλέον, τροποποιήστε την state_update
, ώστε η πίστα να έχει φαινομενικά
“άπειρο” μήκος, ως εξής:
- Όταν το αεροσκάφος φτάσει κοντά (απόσταση μίας οθόνης) από την τελευταία
γέφυρα, δημιουργούνται νέα αντικείμενα, ξεκινώντας από το
y
της γέφυρας αυτής. - Επίσης η ταχύτητα του παιχνιδιού γίνεται 30% μεγαλύτερη. Αυτό επιτυγχάνεται
αυξάνοντας το
speed_factor
και φροντίζοντας όλες οι μετακινήσεις να λαμβάνουν υπόψη τοspeed_factor
.
Τέλος, επεκτείνετε το tests/test_state.c
ώστε να ελέγχει τη λειτουργία
των πυραύλων. Όπως πάντα, δε χρειάζονται εξαντλητικά tests, αρκεί να ελέγχεται
σύντομα η κίνηση του πυραύλου και η σύγκρουση με εχθρό.
Άσκηση 4 (15%)
Υλοποιήστε ένα module set_utils.h
με τις παρακάτω λειτουργίες:
// Επιστρέφει την μοναδική τιμή του set που είναι ισοδύναμη με value,
// ή αν δεν υπάρχει, την μικρότερη τιμή του set που είναι μεγαλύτερη
// από value. Αν δεν υπάρχει καμία τότε επιστρέφει NULL.
Pointer set_find_eq_or_greater(Set set, Pointer value);
// Επιστρέφει την μοναδική τιμή του set που είναι ισοδύναμη με value,
// ή αν δεν υπάρχει, την μεγαλύτερη τιμή του set που είναι μικρότερη
// από value. Αν δεν υπάρχει καμία τότε επιστρέφει NULL.
Pointer set_find_eq_or_smaller(Set set, Pointer value);
Η υλοποίηση πρέπει να είναι αποδοτική, χωρίς να διατρέχει ολόκληρο το set.
Ένας γενικός τρόπος (που δεν απαιτεί γνώση της υλοποίησης του ADTSet
)
είναι ο εξής: πρώτα ελέγχετε
αν το value
υπάρχει ήδη. Αν όχι, τότε το εισάγετε,
χρησιμοποιείτε το νέο στοιχείο για να βρείτε το αμέσως επόμενο/προηγούμενο,
και τέλος το αφαιρείτε.
Επίσης υλοποιήστε ένα test που να ελέγχει (εν συντομία) την υλοποίηση του module.
Άσκηση 5 (20%)
Η υλοποίηση modules/state.c
του module state.h
που φτιάξατε στις προηγούμενες
εργασίες είναι πολύ καλή για να δημιουργήσουμε ένα γρήγορο prototype
του παιχνιδιού, αλλά η αποθήκευση δεδομένων στο
List objects
δημιουργεί σημαντική καθυστέρηση στους αλγορίθμους.
Στην άσκηση αυτή καλείστε να φτιάξετε μια τροποποιημένη υλοποίηση modules/state_alt.c
του ίδιου module, χρησιμοποιώντας οποιονδήποτε ADT είδαμε στο μάθημα, με
τους παρακάτω στόχους:
Η συνάρτηση
state_objects
πρέπει γρήγορα να επιστρέφει τα αντικείμενα που βρίσκονται ανάμεσα σταy_from
καιy_to
, χωρίς να εξετάζει όλα τα αντικείμενα της πίστας.Η
state_update
πρέπει να ενημερώνει μόνο τα αντικείμενα που βρίσκονται σε απόσταση το πολύ 2 οθόνων από το αεροσκάφος (τα υπόλοιπα μπορούν να παραμένουν ακίνητα). Η εύρεση των αντικειμένων δεν πρέπει να εξετάζει όλα τα αντικείμενα της πίστας.Στην
state_update
, ο έλεγχος των συγκρούσεων πρέπει να είναι αποδοτικός:- Οι συγκρούσεις αεροσκάφους/πυραύλων με άλλα αντικείμενα δεν πρέπει να εξετάζει όλα τα αντικείμενα της πίστας.
- Οι συγκρούσεις των εχθρών με το έδαφος πρέπει να γίνεται αποθηκεύοντας, για κάθε εχθρό,
το ελάχιστο και μέγιστο
x
μέσα στα οποία μπορούν να κινούνται. Η εύρεση της πληροφορίας αυτής πρέπει να είναι γρήγορη.
Για την υλοποίησή σας μπορείτε να τροποποιήσετε το state_alt.c
όπως νομίζετε, αλλά καμία
αλλαγή δεν επιτρέπεται στο state.h
(ώστε οι χρήστες του module να συνεχίζουν να δουλεύουν χωρίς
τροποποιήσεις).
Η υλοποίησή σας θα πρέπει επίσης να περνάει το tests/state_test.c
που έχετε φτιάξει στις προηγούμενες ασκήσεις,
χωρίς καμία τροποποίηση.
Άσκηση 6 (15%)
Στο τελικό στάδιο είμαστε πλέον έτοιμοι να υλοποιήσουμε το πλήρες παιχνίδι.
Για το σκοπό αυτό καλείστε να υλοποιήσετε ένα module interface.h
με τις ακόλουθες συναρτήσεις.
// Αρχικοποιεί το interface του παιχνιδιού
void interface_init();
// Κλείνει το interface του παιχνιδιού
void interface_close();
// Σχεδιάζει ένα frame με την τωρινή κατάσταση του παιχνδιού
void interface_draw_frame(State state);
Η βασική συνάρτηση είναι η interface_draw_frame
στην οποία πρέπει αρχικά να συλλέξετε πληροφορίες
για την κατάσταση του παιχνιδιού, χρησιμοποιώντας το state.h
module, και στη συνέχεια να σχεδιάσετε
τα αντικείμενα τα οποία είναι ορατά στο συγκεκριμένο frame.
Για το σχεδιασμό μπορείτε να χρησιμοποιείτε
όλες τις συναρτήσεις του raylib.h
, δείτε το παράδειγμα του programs/game_example
για να ξεκινήσετε.
Πλήρης λίστα με τις συναρτήσεις υπάρχει στο raylib.h
, αλλά και στο παρακάτω
reference. Φυσικά εσείς θα χρειαστείτε ελάχιστες
από αυτές, δείτε κυρίως τις DrawLine, DrawText, DrawRectangleRec, DrawTexture, ...
.
Τα γραφικά δεν χρειάζεται προφανώς να είναι σύνθετα, μπορεί το κάθε αντικείμενο να είναι απλά
ένα χρωματιστό παραλληλόγραμμο, αρκεί το τελικό αποτέλεσμα να είναι playable.
Προσοχή: στην οθόνη θέλετε να σχεδιάσετε μόνο το ορατό μέρος της συνολικής πίστας. Οπότε πρέπει να βρείτε τα αντικείμενα
που είναι ορατά (μέσω της state_objects
) αλλά και να μετατρέψετε τις συντεταγμένες του state σε συντεταγμένες της
οθόνης.
Για να ολοκληρωθεί το παιχνίδι χρειάζεται τέλος και μία συνάρτηση main
η οποία θα ξεκινάει το “main loop”
του παιχνιδιού, καλώντας διαδοχικά τις state_update
και interface_draw_frame
. Και πάλι, δείτε το παράδειγμα του
programs/game_example
. Η συνάρτηση main
πρέπει να βρίσκεται στο αρχείο programs/game/game.c
.
Παρατηρήσεις: Το παιχνίδι θα πρέπει να δουλεύει και με τις δύο υλοποιήσεις του state.h
module που υλοποιήσατε. Για να δείτε τη
διαφορά στην απόδοση, προσθέστε ένα framerate (FPS) counter, και αυξήστε τη σταθερά BRIDGE_NUM
μέχρι
το FPS να πέσει κάτω από 60. Αναφέρετε στο README.md
μια τάξη μεγέθους για τον αριθμό που άντεξε κάθε υλοποίηση.
Επίσης, όπως αναφέρθηκε και στην Άσκηση 3, είστε ελεύθεροι να τροποποιήσετε την υλοποίηση
του state.h
module για να προσαρμόσετε το παιχνίδι στο interface που δημιουργήσατε.
Στο interface του module από την άλλη δεν επιτρέπονται αλλαγές (αλλά έχετε πλήρη ελευθερία
για αλλαγές στο παρακάτω design competition).
Design competition
Αφήστε τη δημιουργικότητά σας να δουλέψει και εξελίξτε το παιχνίδι με οποιοδήποτε τρόπο θέλετε! Βάλτε νέους χαρακτήρες, εξελίξτε το gameplay, φτιάξτε καλύτερα physics, βελτιώστε τα γραφικά, προσθέστε storyline, σχεδιάστε πίστες, animations, ή οτιδήποτε άλλο θέλετε.
Νικητής του διαγωνισμού θα είναι απλά το πιο ευχάριστο παιχνίδι. Αυτό δε σημαίνει το πιο σύνθετο τεχνικά, συχνά τα απλά παιχνίδια είναι και τα πιο εθιστικά. Η επιλογή θα γίνει με ψηφοφορία (ίσως μετά από προεπιλογή, αν οι συμμετοχές είναι πάρα πολλές). Όλοι οι συμμετέχοντες μπορούν να κερδίσουν bonus έως 25% στο βαθμό της εργασίας (ανάλογα με τις βελτιώσεις που έχουν υλοποιήσει), ενώ οι 2 πρώτοι κερδίζουν bonus 50% και 100% αντίστοιχα.
Για να συμμετέχετε στο διαγωνισμό, φτιάξτε το παιχνίδι σας στο directory
programs/competition
, και βεβαιωθείτε ότι τρέχοντας make game
στο directory
αυτό παράγεται το εκτελέσιμο game
του παιχνιδιού. Τα περιεχόμενα του directory
δε θα ληφθούν υπόψη στη βαθμολόγηση παρά μόνο στο διαγωνισμό.
Για τις βελιτώσεις του παιχνιδιού έχετε προθεσμία μέχρι το deadline της
δεύτερης εργασίας. Αλλαγές στo repository που θα γίνουν μετά την προθεσμία της
πρώτης εργασίας, και πριν την προθεσμία της δεύτερης, θα ληφθούν υπόψη για τον διαγωνισμό
αλλά όχι για τη βαθμολόγηση της εργασίας.
Επίσης περιγράψτε τις βελτιώσεις που υλοποιήσατε στο README.md
.
Game Competition
Τέλος, στο παιχνίδι που θα κερδίσει τον παραπάνω διαγωνισμό, θα ακολουθήσει game competition για να αποδείξετε ότι είστε ο καλύτερος gamer του DI! Ο διαγωνισμός θα έχει χαρακτήρα esport, θα μεταδοθεί live και θα μπορεί να συμμετέχει οποιοσδήποτε φοιτητής του τμήματος. Περισσότερες λεπτομέρειες θα ανακοινωθούν μετά την παράδοση της εργασίας.
Χρήση σε Windows/WSL
Για να κάνετε compile το game_example
σε WSL:
Αρχικά ακολουθήστε τις οδηγίες εγκατάστασης και εκτελέστε το
curl https://k08.chatzi.org/vscode/config.sh | bash
(ίσως χρειαστεί να το ξανατρέξετε, αν δεν το έχετε κάνει πρόσφατα).
Στη συνέχεια εγκαταστήστε το VcXsrv το οποίο επιτρέπει να τρέχουμε Linux προγράμματα με γραφικό interface στο WSL.
Αφού το εγκαταστήσετε, εκτελέστε το VcXsrv, επιλέξτε τις default ρυθμίσεις, και κρατήστε το ανοικτό όσο δουλεύετε.
Τέλος κάνουμε compile/debug από το VS Code ως συνήθως (
Ctrl-Shift-B
,F5
, κλπ).
Αν θέλουμε να τρέξουμε το παιχνίδι manually από την κονσόλα, εκτελούμε πιο πριν export DISPLAY=:0
(το Makefile
το κάνει αυτό αυτόματα).
Τρέχοντας το παιχνίδι μέσα από WSL δεν θα έχει ήχο, αλλά δεν είναι απαραίτητος ο ήχος για την εργασία. Οι πιο τολμηροί μπορούν να δοκιμάσουν τις παρακάτω οδηγίες για ήχο σε WSL (αλλά ίσως είναι απλούστερο απλά να χρησιμοποιήσετε Linux).
Τέλος το Makefile
που σας δίνεται επιτρέπει να παράγετε και native executables (.exe
) μέσα από το WSL, τα οποία υποστηρίζουν και ήχο:
sudo apt install gcc-mingw-w64-x86-64
make OS=Windows_NT